",
7 | "license": "GPLv3",
8 | "private": true,
9 | "dependencies": {
10 | "about-window": "^1.15.2",
11 | "electron-json-storage": "^4.5.0",
12 | "electron-log": "^4.4.6",
13 | "electron-progressbar": "^2.0.1",
14 | "electron-updater": "5.0.6",
15 | "electron-windows-badge": "^1.1.0",
16 | "titlebar": "saghul/titlebar.git"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/build/afterSignHook.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const fs = require('fs');
3 | const path = require('path');
4 | const { notarize } = require('@electron/notarize');
5 |
6 | module.exports = async function(params) {
7 | // Only notarize the app on Mac OS only.
8 | if (params.electronPlatformName !== 'darwin') {
9 | return;
10 | }
11 |
12 | if (!process.env.APPLE_ID) {
13 | console.warn('Cannot find appleId in env to notarize application, skipping notarization');
14 | return;
15 | }
16 | console.log('afterSign hook triggered', params);
17 |
18 | // Same appId in electron-builder.
19 | let appId = 'com.agprojects.Sylk'
20 |
21 | let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);
22 | if (!fs.existsSync(appPath)) {
23 | throw new Error(`Cannot find application at: ${appPath}`);
24 | }
25 |
26 | if (!process.env.APPLE_ID_PASSWORD) {
27 | throw new Error('Cannot find appleIdPassword in env to notarize application');
28 | }
29 |
30 | console.log(`Notarizing ${appId} found at ${appPath}`);
31 |
32 | try {
33 | await notarize({
34 | appBundleId: appId,
35 | appPath: appPath,
36 | appleId: process.env.APPLE_ID,
37 | appleIdPassword: process.env.APPLE_ID_PASSWORD,
38 | teamId: process.env.TEAM_ID
39 | });
40 | } catch (error) {
41 | console.error(error);
42 | }
43 |
44 | console.log(`Done notarizing ${appId}`);
45 | };
46 |
--------------------------------------------------------------------------------
/build/entitlements.mac.inherit.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.inherit
6 |
7 | com.apple.security.cs.allow-jit
8 |
9 | com.apple.security.cs.allow-dyld-environment-variables
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/build/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.device.bluetooth
6 |
7 | com.apple.security.device.camera
8 |
9 | com.apple.security.device.microphone
10 |
11 | com.apple.security.device.audio-input
12 |
13 | com.apple.security.device.usb
14 |
15 | com.apple.security.network.client
16 |
17 | com.apple.security.network.server
18 |
19 | com.apple.security.cs.allow-jit
20 |
21 | com.apple.security.cs.allow-unsigned-executable-memory
22 |
23 | com.apple.security.cs.allow-dyld-environment-variables
24 |
25 | com.apple.security.application-groups
26 |
27 | com.agprojects.Sylk
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/build/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/build/icon.icns
--------------------------------------------------------------------------------
/build/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/build/icon.ico
--------------------------------------------------------------------------------
/build/icons/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/build/icons/512.png
--------------------------------------------------------------------------------
/configure:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | yarn install
3 |
--------------------------------------------------------------------------------
/examples/apache/.htaccess:
--------------------------------------------------------------------------------
1 |
2 | RewriteEngine On
3 | RewriteBase /
4 | RewriteRule ^index\.html$ - [L]
5 | RewriteCond %{REQUEST_FILENAME} !-f
6 | RewriteCond %{REQUEST_FILENAME} !-d
7 | RewriteRule . /index.html [L]
8 |
9 |
--------------------------------------------------------------------------------
/src/app/MaterialColors.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { colors } = require('@material-ui/core');
4 | const murmur = require('murmurhash-js');
5 |
6 | // Available material design colors
7 | const availableColors = [
8 | colors.red, colors.pink, colors.purple, colors.deepPurple,
9 | colors.indigo, colors.blue, colors.lightBlue, colors.cyan,
10 | colors.teal, colors.green, colors.lightGreen, colors.lime,
11 | colors.yellow, colors.amber, colors.orange, colors.deepOrange,
12 | colors.brown, colors.grey, colors.blueGrey
13 | ];
14 |
15 | function generateColor(text) {
16 | return availableColors[(murmur.murmur3(text) % availableColors.length)];
17 | }
18 |
19 | exports.generateColor = generateColor;
20 |
--------------------------------------------------------------------------------
/src/app/MaterialUIAsBootstrap.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const { withStyles, makeStyles } = require('@material-ui/core/styles');
5 | const { Button, Tab, Tabs, Tooltip } = require('@material-ui/core');
6 | const { InputBase } = require('@material-ui/core');
7 |
8 | const BootstrapButton = withStyles({
9 | root: {
10 | boxShadow: 'none',
11 | textTransform: 'none',
12 | fontSize: 14,
13 | fontFamily: 'inherit',
14 | fontWeight: 'normal',
15 | padding: '6px 12px',
16 | border: '1px solid transparent',
17 | lineHeight: 1.42857143,
18 | borderRadius: '4px'
19 | },
20 | contained: {
21 | backgroundColor: '#337ab7',
22 | borderColor: '#2e6da4',
23 | color: '#fff',
24 | '&:hover': {
25 | backgroundColor: '#286090',
26 | borderColor: '#204d74',
27 | boxShadow: 'none'
28 | },
29 | '&:active': {
30 | boxShadow: 'inset 0 3px 5px rgba(0, 0, 0, .125)',
31 | backgroundColor: '#286090',
32 | borderColor: '#204d74'
33 | },
34 | '&:focus': {
35 | borderColor: '#122b40',
36 | backgroundColor: '#204d74',
37 | outlineOffset: '-2px',
38 | boxShadow: 'inset 0px 3px 5px 0px rgba(0,0,0,.125)'
39 | }
40 | },
41 | disabled: {
42 | border: '1px solid #fff',
43 | cursor: 'not-allowed'
44 | },
45 | sizeLarge: {
46 | padding: '10px 20px',
47 | fontSize: 18,
48 | fontWeight: 'bold',
49 | borderRadius: '6px',
50 | lineHeight: 1.33333
51 | },
52 | label: {
53 | display: 'block'
54 | },
55 | text: {
56 | borderColor: 'transparent',
57 | backgroundColor: 'transparent',
58 | color: '#337ab7',
59 | boxShadow: 'none'
60 | }
61 | })(Button);
62 |
63 |
64 | const BootstrapInputBase = withStyles((theme) => ({
65 | root: {
66 | 'label + &': {
67 | marginTop: theme.spacing(3)
68 | },
69 | fontFamily: 'inherit'
70 | },
71 | input: {
72 | borderRadius: 4,
73 | position: 'relative',
74 | backgroundColor: theme.palette.background.paper,
75 | border: '1px solid #ced4da',
76 | fontSize: 14,
77 | padding: '10px 26px 10px 12px',
78 | transition: theme.transitions.create(['border-color', 'box-shadow']),
79 | boxShadow: 'inset 0 1px 1px rgba(0, 0, 0, .075)',
80 | '&:focus': {
81 | borderRadius: 4,
82 | borderColor: '#66afe9',
83 | boxShadow: 'inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6)'
84 | }
85 | }
86 | }))(InputBase);
87 |
88 | const BootstrapTabs = withStyles({
89 | root: {
90 | borderBottom: '1px solid rgba(0, 0, 0, .12)'
91 | },
92 | indicator: {
93 | backgroundColor: '#337ab7'
94 | }
95 | })(Tabs);
96 |
97 | const BootstrapTab = withStyles((theme) => ({
98 | root: {
99 | textTransform: 'none',
100 | fontWeight: theme.typography.fontWeightRegular,
101 | fontFamily: 'inherit',
102 | fontSize: '14px',
103 | '&:hover': {
104 | color: '#23527c',
105 | opacity: 1
106 | },
107 | '&$selected': {
108 | color: '#337ab7',
109 | fontWeight: theme.typography.fontWeightMedium
110 | },
111 | '&:focus': {
112 | color: '#337ab7'
113 | }
114 | },
115 | selected: {}
116 | }))(Tab);
117 |
118 |
119 | const useStylesBootstrap = makeStyles((theme) => ({
120 | arrow: {
121 | color: theme.palette.common.black
122 | },
123 | tooltip: {
124 | backgroundColor: theme.palette.common.black,
125 | fontSize: '12px',
126 | fontFamily: 'inherit'
127 | }
128 | }));
129 |
130 | function BootstrapTooltip(props) {
131 | const classes = useStylesBootstrap();
132 |
133 | return ;
134 | }
135 |
136 | exports.Button = BootstrapButton;
137 | exports.InputBase = BootstrapInputBase;
138 | exports.Tab = BootstrapTab;
139 | exports.Tabs = BootstrapTabs;
140 | exports.Tooltip = BootstrapTooltip;
141 |
--------------------------------------------------------------------------------
/src/app/components/AboutModal.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const ReactBootstrap = require('react-bootstrap');
6 | const Modal = ReactBootstrap.Modal;
7 |
8 |
9 | const AboutModal = (props) => {
10 | return (
11 |
12 |
13 | About Sylk
14 |
15 |
16 |
17 | Sylk client is part of Sylk Suite , a set of
18 | applications for real-time communications using SIP and WebRTC specifications
19 |
20 |
21 | Copyright © AG Projects
22 |
23 |
24 | );
25 | }
26 |
27 | AboutModal.propTypes = {
28 | show: PropTypes.bool.isRequired,
29 | close: PropTypes.func.isRequired
30 | };
31 |
32 |
33 | module.exports = AboutModal;
34 |
--------------------------------------------------------------------------------
/src/app/components/AudioPlayer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | // const play = require('audio-play');
6 | const ac = require('audio-context')();
7 |
8 | const utils = require('../utils');
9 |
10 |
11 | class AudioPlayer extends React.Component {
12 | constructor(props) {
13 | super(props);
14 | this.timeout = null;
15 |
16 | this.buffer = null;
17 | this.time = null;
18 | this.src = null
19 |
20 | // ES6 classes no longer autobind
21 | this.audioEnded = this.audioEnded.bind(this);
22 | this.stop = this.stop.bind(this);
23 | }
24 |
25 | componentDidMount() {
26 | utils.loadAudio(this.props.sourceFile, ac).then(
27 | (buffer) => {
28 | this.buffer = buffer
29 | this.time = ac.currentTime;
30 | }
31 | );
32 | }
33 |
34 | audioEnded() {
35 | this.timeout = setTimeout(() => {
36 | let source = ac.createBufferSource();
37 | source.buffer = this.buffer;
38 | source.addEventListener('ended', this.audioEnded);
39 | source.connect(ac.destination);
40 | source.start(this.time || ac.currentTime);
41 | this.src = source;
42 | }, 3000);
43 | }
44 |
45 | componentWillUnmount() {
46 | clearTimeout(this.timeout);
47 | this.timeout = null;
48 | if (this.src !== null) {
49 | this.src.removeEventListener('ended', this.audioEnded);
50 | this.src = null;
51 | }
52 | }
53 |
54 | play(repeat) {
55 | let source = ac.createBufferSource();
56 | source.buffer = this.buffer;
57 |
58 | if (repeat) {
59 | this.timeout = null;
60 | source.addEventListener('ended', this.audioEnded);
61 | } else {
62 | source.addEventListener('ended', this.stop);
63 | }
64 | source.connect(ac.destination);
65 | source.start(this.time || ac.currentTime);
66 | this.src = source;
67 | }
68 |
69 | stop() {
70 | if (this.src !== null) {
71 | // this.src.stop();
72 | this.src.removeEventListener('ended', this.audioEnded);
73 | this.src = null;
74 | }
75 | clearTimeout(this.timeout);
76 | this.timeout = null;
77 | }
78 |
79 | render() {
80 | return (
);
81 | }
82 | }
83 |
84 | AudioPlayer.propTypes = {
85 | sourceFile: PropTypes.string.isRequired
86 | };
87 |
88 |
89 | module.exports = AudioPlayer;
90 |
--------------------------------------------------------------------------------
/src/app/components/CallByUriBox.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const { default: clsx } = require('clsx');
6 |
7 | const Call = require('./Call');
8 | const FooterBox = require('./FooterBox');
9 | const PreMedia = require('./PreMedia');
10 |
11 | class CallByUriBox extends React.Component {
12 | constructor(props) {
13 | super(props);
14 | this.state = {
15 | displayName: ''
16 | };
17 |
18 | this._notificationCenter = null;
19 |
20 | // ES6 classes no longer autobind
21 | this.handleDisplayNameChange = this.handleDisplayNameChange.bind(this);
22 | this.handleSubmit = this.handleSubmit.bind(this);
23 | this.callStateChanged = this.callStateChanged.bind(this);
24 | }
25 |
26 | componentDidMount() {
27 | if (!this.props.localMedia) {
28 | this.props.getLocalMedia();
29 | }
30 | this._notificationCenter = this.props.notificationCenter();
31 | }
32 |
33 | componentDidUpdate(prevProps, prevState) {
34 | if (!prevProps.currentCall && this.props.currentCall) {
35 | this.props.currentCall.on('stateChanged', this.callStateChanged);
36 | }
37 | }
38 |
39 | callStateChanged(oldState, newState, data) {
40 | if (newState === 'terminated') {
41 | this._notificationCenter.postSystemNotification('Thanks for calling with Sylk!', {timeout: 10});
42 | }
43 | }
44 |
45 | handleDisplayNameChange(event) {
46 | this.setState({displayName: event.target.value});
47 | }
48 |
49 | handleSubmit(event) {
50 | event.preventDefault();
51 | this.props.handleCallByUri(this.state.displayName, this.props.targetUri);
52 | }
53 |
54 | render() {
55 | const validInput = this.state.displayName !== '';
56 | let content;
57 |
58 | if (this.props.account !== null && this.props.localMedia) {
59 | content = (
60 |
70 | );
71 | } else {
72 | const classes = clsx({
73 | 'capitalize' : true,
74 | 'btn' : true,
75 | 'btn-lg' : true,
76 | 'btn-block' : true,
77 | 'btn-default': !validInput,
78 | 'btn-primary': validInput
79 | });
80 |
81 | content = (
82 |
83 |
86 |
You've been invited to call{this.props.targetUri}
87 |
103 |
104 | );
105 | }
106 |
107 | return (
108 |
109 | {!this.props.account && this.props.localMedia &&
}
110 |
111 | {content}
112 |
113 |
114 | );
115 | }
116 | }
117 |
118 | CallByUriBox.propTypes = {
119 | handleCallByUri : PropTypes.func.isRequired,
120 | notificationCenter : PropTypes.func.isRequired,
121 | hangupCall : PropTypes.func.isRequired,
122 | setDevice : PropTypes.func.isRequired,
123 | shareScreen : PropTypes.func.isRequired,
124 | getLocalMedia : PropTypes.func.isRequired,
125 | targetUri : PropTypes.string,
126 | localMedia : PropTypes.object,
127 | account : PropTypes.object,
128 | currentCall : PropTypes.object,
129 | generatedVideoTrack : PropTypes.bool
130 | };
131 |
132 |
133 | module.exports = CallByUriBox;
134 |
--------------------------------------------------------------------------------
/src/app/components/CallCompleteBox.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 |
6 | const Logo = require('./Logo');
7 | const config = require('../config');
8 |
9 |
10 | const CallCompleteBox = (props) => {
11 | return (
12 |
13 |
14 |
15 | { props.targetUri === ''
16 | ?
17 |
We hope you enjoyed this {props.wasCall === true ? 'call' : 'conference'}. If you did, you can try using Sylk Client application:
18 |
Download
19 |
20 |
Or you can {props.wasCall === true ? 'call' : 'join'} again:
21 |
22 | {props.wasCall ? 'Call' : 'Join'}
23 |
24 |
25 | :
26 |
The {props.wasCall === true ? 'call' : 'conference'} cannot be completed at this moment. The reason was: {props.failureReason}
27 |
Try again
28 |
29 | }
30 |
31 |
32 | );
33 | };
34 |
35 |
36 | CallCompleteBox.propTypes = {
37 | wasCall : PropTypes.bool,
38 | targetUri : PropTypes.string,
39 | retryHandler: PropTypes.func,
40 | failureReason: PropTypes.string
41 | };
42 |
43 | module.exports = CallCompleteBox;
44 |
--------------------------------------------------------------------------------
/src/app/components/CallMeMaybeModal.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const ReactBootstrap = require('react-bootstrap');
6 | const Modal = ReactBootstrap.Modal;
7 |
8 | const utils = require('../utils');
9 |
10 |
11 | class CallMeMaybeModal extends React.Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | // ES6 classes no longer autobind
16 | this.handleClipboardButton = this.handleClipboardButton.bind(this);
17 | this.handleEmailButton = this.handleEmailButton.bind(this);
18 |
19 | const sipUri = this.props.callUrl.split('/').slice(-1)[0]; // hack!
20 | const emailMessage = `You can call me using a Web browser at ${this.props.callUrl} or a SIP client at ${sipUri} ` +
21 | 'or Sylk app from https://sylkserver.com';
22 | const subject = 'Call me, maybe?';
23 |
24 | this.emailLink = `mailto:?subject=${encodeURI(subject)}&body=${encodeURI(emailMessage)}`;
25 | }
26 |
27 | handleClipboardButton(event) {
28 | utils.copyToClipboard(this.props.callUrl);
29 | this.props.notificationCenter().postSystemNotification('Call me, maybe?', {body: 'URL copied to the clipboard'});
30 | this.props.close();
31 | }
32 |
33 | handleEmailButton(event) {
34 | if (navigator.userAgent.indexOf('Chrome') > 0) {
35 | let emailWindow = window.open(this.emailLink, '_blank');
36 | setTimeout(() => {
37 | emailWindow.close();
38 | }, 500);
39 | } else {
40 | window.open(this.emailLink, '_self');
41 | }
42 | this.props.close();
43 | }
44 |
45 | render() {
46 |
47 | return (
48 |
49 |
50 | Call me, maybe?
51 |
52 |
53 |
54 | Share this link with others so they can easily call you.
55 |
56 | You can copy it to the clipboard or send it via email.
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 | }
72 | }
73 |
74 | CallMeMaybeModal.propTypes = {
75 | show : PropTypes.bool.isRequired,
76 | close : PropTypes.func.isRequired,
77 | callUrl : PropTypes.string.isRequired,
78 | notificationCenter : PropTypes.func.isRequired
79 | };
80 |
81 |
82 | module.exports = CallMeMaybeModal;
83 |
--------------------------------------------------------------------------------
/src/app/components/Chat/OldMessage.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const useState = React.useState;
5 | const useEffect = React.useEffect;
6 | const PropTypes = require('prop-types');
7 | const ReactBootstrap = require('react-bootstrap');
8 | const Media = ReactBootstrap.Media;
9 | const { default: parse } = require('html-react-parser');
10 | const linkifyUrls = require('linkify-urls');
11 | const { Chip } = require('@material-ui/core');
12 | const { makeStyles } = require('@material-ui/core/styles');
13 | const {
14 | Lock: LockIcon
15 | } = require('@material-ui/icons');
16 |
17 |
18 | const styleSheet = makeStyles((theme) => ({
19 | chipSmall: {
20 | height: 18,
21 | fontSize: 11
22 | },
23 | iconSmall: {
24 | width: 12,
25 | height: 12
26 | },
27 | lockIcon: {
28 | fontSize: 15,
29 | verticalAlign: 'middle',
30 | color: '#ccc'
31 | }
32 | }));
33 |
34 | const Message = ({
35 | message
36 | }) => {
37 | const classes = styleSheet();
38 | const [parsedContent, setParsedContent] = useState();
39 |
40 | const preHtmlEntities = (str) => {
41 | return String(str).replace(//g, '>').replace(/"/g, '"');
42 | };
43 |
44 | const postHtmlEntities = (str) => {
45 | return String(str).replace(/(?!&|<|>|")&/g, '&');
46 | };
47 |
48 | const customUrlRegexp = () => (/((?:https?(?::\/\/))(?:www\.)?(?:[a-zA-Z\d-_.]+(?:(?:\.|@)[a-zA-Z\d]{2,})|localhost)(?:(?:[-a-zA-Z\d:%_+.~#!?&//=@();]*)(?:[,](?![\s]))*)*)/g);
49 |
50 | useEffect(() => {
51 | if (message.contentType === 'text/html') {
52 | setParsedContent(parse(message.content.trim(), {
53 | replace: (domNode) => {
54 | if (domNode.attribs && domNode.attribs.href) {
55 | domNode.attribs.target = '_blank';
56 | return;
57 | }
58 | if (domNode.type === 'text') {
59 | if (!domNode.parent || (domNode.parent.type === 'tag' && domNode.parent.name !== 'a')) {
60 | let url = linkifyUrls(preHtmlEntities(domNode.data), {
61 | customUrlRegexp,
62 | attributes: {
63 | target: '_blank',
64 | rel: 'noopener noreferrer'
65 | }
66 | });
67 | return ({parse(postHtmlEntities(url))} );
68 | }
69 | }
70 | }
71 | }));
72 | } else if (message.contentType.startsWith('image/')) {
73 | const image = `data:${message.contentType};base64,${message.content}`
74 | setParsedContent( );
75 | } else if (message.contentType === 'text/plain') {
76 | const linkfiedContent = linkifyUrls(preHtmlEntities(message.content), {
77 | customUrlRegexp,
78 | attributes: {
79 | target: '_blank',
80 | rel: 'noopener noreferrer'
81 | }
82 | })
83 |
84 | setParsedContent(
85 | {parse(postHtmlEntities(linkfiedContent))}
86 | );
87 | } else if (message.contentType === 'text/pgp-public-key') {
88 | setParsedContent(
89 | }
95 | label="Public key"
96 | />
97 | );
98 | }
99 | }, [message, classes]) // eslint-disable-line react-hooks/exhaustive-deps
100 |
101 | return (
102 |
103 |
104 | Edit message
105 |
106 | {parsedContent}
107 |
108 | );
109 | };
110 |
111 | Message.propTypes = {
112 | message: PropTypes.object.isRequired
113 | };
114 |
115 |
116 | module.exports = Message;
117 |
--------------------------------------------------------------------------------
/src/app/components/Chat/VoiceMessageRecorderModal.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const { useEffect, useRef, useState } = React;
5 | const PropTypes = require('prop-types');
6 |
7 | const {
8 | IconButton,
9 | Popover
10 | } = require('@material-ui/core');
11 | const {
12 | CancelRounded: CancelIcon
13 | } = require('@material-ui/icons');
14 | const { makeStyles } = require('@material-ui/core/styles');
15 |
16 | const { useReactMediaRecorder } = require('react-media-recorder');
17 |
18 | const VoiceMessageRecorderRenderer = require('./VoiceMessageRecorderRenderer');
19 |
20 |
21 | const styleSheet = makeStyles({
22 | top: {
23 | position: 'absolute',
24 | top: -20,
25 | right: -20
26 | },
27 | iconSize: {
28 | fontSize: '2rem'
29 | },
30 | popoverRoot: {
31 | overflow: 'visible'
32 | }
33 | });
34 |
35 | const VoiceMessageRecorderModal = (props) => {
36 | const classes = styleSheet();
37 |
38 | const { status, startRecording, stopRecording, previewAudioStream } =
39 | useReactMediaRecorder({
40 | video: false,
41 | mediaRecorderOptions: { mimeType: 'audio/wav' },
42 | onStop: (t, u) => {
43 | recordingStopped(t, u)
44 | }
45 | });
46 |
47 | const [voiceMessage, setVoiceMessage] = useState(null);
48 | const [isPreviewStarted, setIsPreviewStarted] = useState(false);
49 |
50 | const shouldUnmount = useRef(true);
51 |
52 | useEffect(() => {
53 | let ignore = false;
54 | if (props.show && startRecording && !isPreviewStarted) {
55 | if (!ignore) {
56 | setIsPreviewStarted(true);
57 | startRecording();
58 | }
59 | }
60 | return () => {
61 | ignore = true;
62 | }
63 | }, [startRecording, isPreviewStarted, props.show]);
64 |
65 | const recordingStopped = (blobUrl, blob) => {
66 | if (shouldUnmount.current !== false) {
67 | setVoiceMessage(blob);
68 | } else {
69 | props.close();
70 | }
71 | }
72 |
73 | const handleClose = () => {
74 | shouldUnmount.current = false;
75 |
76 | if (status !== 'stopped') {
77 | stopRecording();
78 | } else {
79 | props.close();
80 | }
81 | }
82 |
83 | return (
84 |
101 |
102 | {
113 | props.sendAudioMessage([
114 | new File(
115 | [voiceMessage],
116 | 'sylk-audio-recording.' + voiceMessage.type.split(';')[0].split('/')[1] || 'webm',
117 | { type: voiceMessage.type }
118 | )
119 | ]);
120 | handleClose();
121 | }}
122 | />
123 |
124 |
125 |
126 |
127 |
128 |
129 | );
130 | }
131 |
132 | VoiceMessageRecorderModal.propTypes = {
133 | show: PropTypes.bool.isRequired,
134 | close: PropTypes.func.isRequired,
135 | sendAudioMessage: PropTypes.func.isRequired,
136 | anchorElement: PropTypes.object.isRequired
137 | };
138 |
139 |
140 | module.exports = VoiceMessageRecorderModal;
141 |
--------------------------------------------------------------------------------
/src/app/components/Conference.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const assert = require('assert');
6 | const debug = require('debug');
7 |
8 | const ConferenceBox = require('./ConferenceBox');
9 | const LocalMedia = require('./LocalMedia');
10 | const config = require('../config');
11 |
12 | const DEBUG = debug('blinkrtc:Conference');
13 |
14 |
15 | class Conference extends React.Component {
16 | constructor(props) {
17 | super(props);
18 |
19 | // ES6 classes no longer autobind
20 | this.mediaPlaying = this.mediaPlaying.bind(this);
21 | this.confStateChanged = this.confStateChanged.bind(this);
22 | this.hangup = this.hangup.bind(this);
23 | }
24 |
25 | confStateChanged(oldState, newState, data) {
26 | DEBUG(`Conference state changed ${oldState} -> ${newState}`);
27 | if (newState === 'established') {
28 | this.forceUpdate();
29 | }
30 | }
31 |
32 | start() {
33 | assert(this.props.currentCall == null, 'currentCall is not null');
34 | const options = {
35 | pcConfig: {iceServers: config.iceServers},
36 | localStream: this.props.localMedia,
37 | offerOptions: {
38 | offerToReceiveAudio: false,
39 | offerToReceiveVideo: false
40 | },
41 | initialParticipants: this.props.participantsToInvite
42 | };
43 | Object.assign(options, this.props.roomMedia);
44 | const confCall = this.props.account.joinConference(this.props.targetUri.toLowerCase(), options);
45 | confCall.on('stateChanged', this.confStateChanged);
46 | }
47 |
48 | hangup() {
49 | this.props.hangupCall();
50 | }
51 |
52 | mediaPlaying() {
53 | assert(this.props.currentCall == null, 'currentCall is not null');
54 | this.start();
55 | }
56 |
57 |
58 | render() {
59 | let box;
60 |
61 | if (this.props.localMedia !== null) {
62 | if (this.props.currentCall != null && this.props.currentCall.state === 'established') {
63 | box = (
64 |
79 | );
80 | } else {
81 | box = (
82 |
89 | );
90 | }
91 | }
92 |
93 | return (
94 |
95 | {box}
96 |
97 | );
98 | }
99 | }
100 |
101 | Conference.propTypes = {
102 | notificationCenter : PropTypes.func.isRequired,
103 | account : PropTypes.object.isRequired,
104 | hangupCall : PropTypes.func.isRequired,
105 | setDevice : PropTypes.func.isRequired,
106 | shareScreen : PropTypes.func.isRequired,
107 | propagateKeyPress : PropTypes.func.isRequired,
108 | toggleShortcuts : PropTypes.func.isRequired,
109 | currentCall : PropTypes.object,
110 | localMedia : PropTypes.object,
111 | targetUri : PropTypes.string,
112 | participantsToInvite : PropTypes.array,
113 | generatedVideoTrack : PropTypes.bool,
114 | muteAudioFromStart : PropTypes.bool,
115 | participantIsGuest : PropTypes.bool,
116 | roomMedia : PropTypes.object,
117 | lowBandwidth : PropTypes.bool,
118 | toggleChatInCall : PropTypes.func,
119 | unreadMessages : PropTypes.object
120 |
121 | };
122 |
123 |
124 | module.exports = Conference;
125 |
--------------------------------------------------------------------------------
/src/app/components/ConferenceCarousel.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const { default: TransitionGroup } = require('react-transition-group/TransitionGroup');
6 | const { default: CSSTransition } = require('react-transition-group/CSSTransition');
7 | const { default: clsx } = require('clsx');
8 |
9 | class ConferenceCarousel extends React.Component {
10 | constructor(props) {
11 | super(props);
12 | this.state = {
13 | displayLeftArrow: false,
14 | displayRightArrow: false
15 | };
16 |
17 | this.carouselList = null;
18 |
19 | // ES6 classes no longer autobind
20 | this.handleScroll = this.handleScroll.bind(this);
21 | this.handleResize = this.handleResize.bind(this);
22 | this.scrollToRight = this.scrollToRight.bind(this);
23 | this.scrollToLeft = this.scrollToLeft.bind(this);
24 | }
25 |
26 | componentDidMount() {
27 | // Get UL from children of the carousel
28 | const children = this.refs.carousel.children;
29 | for (let child of children) {
30 | if (child.tagName == 'UL') {
31 | this.carouselList = child;
32 | }
33 | };
34 |
35 | if (this.canScroll()) {
36 | this.setState({displayRightArrow: true}); // eslint-disable-line react/no-did-mount-set-state
37 | }
38 |
39 | window.addEventListener('resize', this.handleResize);
40 | }
41 |
42 | componentWillUnmount() {
43 | window.removeEventListener('resize', this.handleResize);
44 | }
45 |
46 | componentDidUpdate(prevProps) {
47 | if (prevProps.children.length != this.props.children.length) {
48 | // We need to wait for the animation to end before calculating
49 | setTimeout(() => {
50 | this.handleScroll();
51 | }, 310);
52 | }
53 | }
54 |
55 | canScroll() {
56 | return (this.carouselList.scrollWidth > this.carouselList.clientWidth);
57 | }
58 |
59 | handleScroll(event) {
60 | const newState = {
61 | displayRightArrow : false,
62 | displayLeftArrow : false
63 | };
64 |
65 | if (this.canScroll()) {
66 | const scrollWidth = this.carouselList.scrollWidth;
67 | const scrollLeft = this.carouselList.scrollLeft;
68 | const clientWidth = this.carouselList.clientWidth;
69 | newState.displayRightArrow = true;
70 | if (scrollLeft > 0) {
71 | newState.displayLeftArrow = true;
72 | if (scrollLeft === (scrollWidth - clientWidth)) {
73 | newState.displayRightArrow = false;
74 | }
75 | } else {
76 | newState.displayLeftArrow = false;
77 | }
78 | }
79 |
80 | this.setState(newState);
81 | }
82 |
83 | scrollToRight(event) {
84 | this.carouselList.scrollLeft += 100;
85 | }
86 |
87 | scrollToLeft(event) {
88 | this.carouselList.scrollLeft -= 100;
89 | }
90 |
91 | handleResize(event) {
92 | if (this.canScroll()) {
93 | this.setState({displayRightArrow: true})
94 | } else {
95 | if (this.state.displayRightArrow) {
96 | this.setState({displayRightArrow: false});
97 | }
98 | }
99 | }
100 |
101 | render() {
102 | const items = [];
103 | let idx = 0;
104 | React.Children.forEach(this.props.children, (child) => {
105 | items.push({child} );
106 | idx++;
107 | });
108 |
109 | const arrows = [];
110 | if (this.state.displayLeftArrow) {
111 | arrows.push(
);
112 | }
113 | if (this.state.displayRightArrow) {
114 | arrows.push(
);
115 | }
116 | const classes = clsx({
117 | 'carousel-list' : true,
118 | 'list-inline' : true,
119 | 'text-right' : this.props.align === 'right'
120 | });
121 | return (
122 |
123 | {arrows}
124 |
125 |
126 | {items}
127 |
128 |
129 |
130 | );
131 | }
132 | }
133 |
134 | ConferenceCarousel.propTypes = {
135 | children: PropTypes.node,
136 | align: PropTypes.string
137 | };
138 |
139 |
140 | module.exports = ConferenceCarousel;
141 |
--------------------------------------------------------------------------------
/src/app/components/ConferenceChat.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const useEffect = React.useEffect;
5 | const useRef = React.useRef;
6 | const useState = React.useState;
7 | const PropTypes = require('prop-types');
8 | const Message = require('./Chat/Message');
9 |
10 |
11 | const ConferenceChat = (props) => {
12 | const messagesEndRef = useRef(null);
13 |
14 | const scrollToBottom = () => {
15 | messagesEndRef.current.scrollIntoView({behavior: 'smooth'})
16 | }
17 |
18 | useEffect(scrollToBottom, [props.scroll]);
19 |
20 | const [entries, setEntries] = useState([])
21 |
22 | useEffect(() => {
23 | let prevMessage = null;
24 | const entries = props.messages.filter((message) => {
25 | return !message.content.startsWith('?OTRv')
26 | }).map((message, idx) => {
27 | let continues = false;
28 | if (prevMessage !== null && prevMessage.sender.uri == message.sender.uri) {
29 | continues = true;
30 | }
31 | prevMessage = message;
32 | return (
33 |
34 | )
35 | });
36 | setEntries(entries);
37 | }, [props.messages])
38 |
39 | return (
40 |
44 | );
45 | };
46 |
47 | ConferenceChat.propTypes = {
48 | scroll: PropTypes.bool,
49 | messages: PropTypes.array.isRequired
50 | };
51 |
52 |
53 | module.exports = ConferenceChat;
54 |
--------------------------------------------------------------------------------
/src/app/components/ConferenceDrawerFiles.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const utils = require('../utils');
6 |
7 | const ReactBootstrap = require('react-bootstrap');
8 | const ListGroup = ReactBootstrap.ListGroup;
9 | const ListGroupItem = ReactBootstrap.ListGroupItem;
10 |
11 |
12 | const ConferenceDrawerFiles = (props) => {
13 | const entries = props.sharedFiles.slice(0).reverse().map((elem, idx) => {
14 | const uploader = elem.uploader.displayName || elem.uploader.uri || elem.uploader;
15 | const color = utils.generateMaterialColor(elem.uploader.uri || elem.uploader)['300'];
16 | return (
17 |
18 | Shared by {uploader}
19 |
25 |
26 | );
27 | });
28 |
29 | return (
30 |
31 |
Shared Files
32 |
33 | {entries}
34 |
35 |
36 | );
37 | };
38 |
39 | ConferenceDrawerFiles.propTypes = {
40 | sharedFiles: PropTypes.array.isRequired,
41 | downloadFile: PropTypes.func.isRequired,
42 | embed: PropTypes.bool,
43 | wide: PropTypes.bool
44 | };
45 |
46 |
47 | module.exports = ConferenceDrawerFiles;
48 |
--------------------------------------------------------------------------------
/src/app/components/ConferenceDrawerLog.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const { default: clsx } = require('clsx');
6 | const utils = require('../utils');
7 |
8 |
9 | const ConferenceDrawerLog = (props) => {
10 | const entries = props.log.map((elem, idx) => {
11 | const classes = clsx({
12 | 'text-danger' : elem.level === 'error',
13 | 'text-warning' : elem.level === 'warning',
14 | 'log-entry' : true
15 | });
16 |
17 | const originator = elem.originator.displayName || elem.originator.uri || elem.originator;
18 |
19 | const messages = elem.messages.map((message, index) => {
20 | return {message} ;
21 | });
22 |
23 | const color = utils.generateMaterialColor(elem.originator.uri || elem.originator)['300'];
24 | return (
25 |
26 |
{props.log.length - idx}
27 |
28 | {originator} {elem.action} {messages}
29 |
30 |
31 | )
32 | });
33 |
34 | return (
35 |
36 |
Configuration Events
37 |
38 | {entries}
39 |
40 |
41 | );
42 | };
43 |
44 | ConferenceDrawerLog.propTypes = {
45 | log: PropTypes.array.isRequired
46 | };
47 |
48 |
49 | module.exports = ConferenceDrawerLog;
50 |
--------------------------------------------------------------------------------
/src/app/components/ConferenceDrawerMute.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 |
6 |
7 | const ConferenceDrawerMute = (props) => {
8 | return (
9 |
10 |
11 |
12 | Mute everyone
13 |
14 |
15 | );
16 | };
17 |
18 | ConferenceDrawerMute.propTypes = {
19 | muteEverybody: PropTypes.func.isRequired
20 | };
21 |
22 |
23 | module.exports = ConferenceDrawerMute;
24 |
--------------------------------------------------------------------------------
/src/app/components/ConferenceDrawerParticipant.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const useState = React.useState;
5 | const useEffect = React.useEffect;
6 | const PropTypes = require('prop-types');
7 | const ReactBootstrap = require('react-bootstrap');
8 | const Label = ReactBootstrap.Label;
9 | const Media = ReactBootstrap.Media;
10 | const ButtonGroup = ReactBootstrap.ButtonGroup;
11 | const hark = require('hark');
12 |
13 | const UserIcon = require('./UserIcon');
14 | const HandIcon = require('./HandIcon');
15 | const CallQuality = require('./CallQuality');
16 |
17 |
18 | const ConferenceDrawerParticipant = (props) => {
19 | let [active, setActive] = useState(false);
20 | let [speech, setSpeech] = useState(null);
21 | const streams = props.participant.streams;
22 |
23 | React.useEffect(() => {
24 | return () => {
25 | if (speech !== null) {
26 | speech.stop();
27 | setSpeech(null);
28 | }
29 | };
30 | },[speech]);
31 |
32 | if (speech === null && props.enableSpeakingIndication && streams.length > 0 && streams[0].getAudioTracks().length !== 0) {
33 | const options = {
34 | interval: 150,
35 | play: false
36 | };
37 |
38 | const speechEvents = hark(streams[0], options);
39 | speechEvents.on('speaking', () => {
40 | setActive(true);
41 | });
42 | speechEvents.on('stopped_speaking', () => {
43 | setActive(false);
44 | });
45 | setSpeech(speechEvents);
46 | }
47 |
48 | let tag = '';
49 | let callQuality;
50 | if (props.isLocal) {
51 | tag = Myself ;
52 | } else {
53 | if (props.stats) {
54 | callQuality = ( );
55 | }
56 | }
57 | return (
58 |
59 |
60 |
61 |
62 |
63 | {props.participant.identity.displayName || props.participant.identity.uri} {callQuality}
64 |
65 |
66 | props.handleHandSelected(props.participant)}
69 | disableHandToggle={props.disableHandToggle}
70 | drawer
71 | />
72 | {tag}
73 |
74 |
75 | );
76 |
77 | }
78 |
79 | ConferenceDrawerParticipant.propTypes = {
80 | participant: PropTypes.object.isRequired,
81 | raisedHand: PropTypes.number.isRequired,
82 | handleHandSelected: PropTypes.func.isRequired,
83 | disableHandToggle: PropTypes.bool,
84 | isLocal: PropTypes.bool,
85 | enableSpeakingIndication: PropTypes.bool,
86 | stats: PropTypes.object
87 | };
88 |
89 |
90 | module.exports = ConferenceDrawerParticipant;
91 |
--------------------------------------------------------------------------------
/src/app/components/ConferenceDrawerParticipantList.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const ReactBootstrap = require('react-bootstrap');
6 | const ListGroup = ReactBootstrap.ListGroup;
7 | const ListGroupItem = ReactBootstrap.ListGroupItem;
8 |
9 |
10 | const ConferenceDrawerParticipantList = (props) => {
11 | const items = [];
12 | let idx = 0;
13 | React.Children.forEach(props.children, (child) => {
14 | items.push({child} );
15 | idx++;
16 | });
17 |
18 | return (
19 |
20 |
Participants
21 |
22 | {items}
23 |
24 |
25 | );
26 | };
27 |
28 | ConferenceDrawerParticipantList.propTypes = {
29 | children: PropTypes.node
30 | };
31 |
32 |
33 | module.exports = ConferenceDrawerParticipantList;
34 |
--------------------------------------------------------------------------------
/src/app/components/ConferenceHeader.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const useState = React.useState;
5 | const useEffect = React.useEffect;
6 | const useRef = React.useRef;
7 | const PropTypes = require('prop-types');
8 | const { default: clsx } = require('clsx');
9 | const { default: TransitionGroup } = require('react-transition-group/TransitionGroup');
10 | const { default: CSSTransition } = require('react-transition-group/CSSTransition');
11 | const { Duration } = require('luxon');
12 |
13 | const useInterval = (callback, delay) => {
14 | const savedCallback = useRef();
15 |
16 | // Remember the latest callback.
17 | useEffect(() => {
18 | savedCallback.current = callback;
19 | }, [callback]);
20 |
21 | // Set up the interval.
22 | useEffect(() => {
23 | function tick() {
24 | savedCallback.current();
25 | }
26 | if (delay !== null) {
27 | let id = setInterval(tick, delay);
28 | return () => clearInterval(id);
29 | }
30 | }, [delay]);
31 | }
32 |
33 | const ConferenceHeader = (props) => {
34 | let [seconds, setSeconds] = useState(0);
35 |
36 | useInterval(() => {
37 | setSeconds(seconds + 1);
38 | }, 1000);
39 |
40 | const duration = Duration.fromObject({seconds: seconds}).toFormat('hh:mm:ss');
41 |
42 | let videoHeader;
43 | let callButtons;
44 |
45 | const mainClasses = clsx({
46 | 'top-overlay': true,
47 | 'on-top': props.onTop
48 | });
49 | if (props.show) {
50 | const participantCount = props.participants.length + 1;
51 |
52 | const callDetail = (
53 |
54 | {duration}
55 | —
56 | {participantCount} participant{participantCount > 1 ? 's' : ''}
57 | {props.callQuality}
58 |
59 | );
60 |
61 | let electron = false;
62 | if (typeof window.process !== 'undefined') {
63 | if (window.process.versions.electron !== '' && window.process.platform === 'darwin') {
64 | electron = true;
65 | }
66 | }
67 |
68 | const leftButtonClasses = clsx({
69 | 'conference-top-left-buttons': true,
70 | 'electron-margin': electron
71 | });
72 |
73 | const headerClasses = clsx({
74 | 'call-header': true,
75 | 'solid-background': props.transparent === false
76 | });
77 |
78 | videoHeader = (
79 |
84 |
85 |
86 |
87 | {props.buttons.top.left}
88 |
89 |
Conference: {props.remoteIdentity}
90 |
{callDetail}
91 |
92 | {props.buttons.top.right}
93 |
94 |
95 |
96 |
97 |
98 | );
99 |
100 | callButtons = (
101 |
106 |
107 | {props.buttons.bottom}
108 |
109 |
110 | );
111 | }
112 |
113 | return (
114 |
115 |
116 | {videoHeader}
117 | {callButtons}
118 |
119 |
120 | );
121 | }
122 |
123 | ConferenceHeader.propTypes = {
124 | show: PropTypes.bool.isRequired,
125 | remoteIdentity: PropTypes.string.isRequired,
126 | participants: PropTypes.array.isRequired,
127 | buttons: PropTypes.object.isRequired,
128 | transparent: PropTypes.bool,
129 | callQuality: PropTypes.object,
130 | onTop: PropTypes.bool
131 | };
132 |
133 |
134 | module.exports = ConferenceHeader;
135 |
--------------------------------------------------------------------------------
/src/app/components/ConferenceMenu.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const { makeStyles } = require('@material-ui/core/styles');
6 | const { Menu, MenuItem, ListItemIcon } = require('@material-ui/core');
7 |
8 |
9 | const styleSheet = makeStyles({
10 | item: {
11 | fontSize: '14px',
12 | color: '#333',
13 | minHeight: 0,
14 | lineHeight: '20px'
15 | },
16 | icon: {
17 | minWidth: '20px'
18 | }
19 | });
20 |
21 | const ConferenceMenu = (props) => {
22 | const classes = styleSheet();
23 |
24 | const handleShortcut = (event) => {
25 | props.toggleShortcuts();
26 | props.close(event);
27 | };
28 |
29 | const handleDevices = (event) => {
30 | props.toggleDevices();
31 | props.close(event);
32 | };
33 | return (
34 |
35 |
58 |
59 | );
60 | }
61 |
62 | ConferenceMenu.propTypes = {
63 | show: PropTypes.bool.isRequired,
64 | close: PropTypes.func.isRequired,
65 | anchor: PropTypes.object,
66 | toggleShortcuts: PropTypes.func,
67 | toggleDevices: PropTypes.func
68 | };
69 |
70 |
71 | module.exports = ConferenceMenu;
72 |
--------------------------------------------------------------------------------
/src/app/components/ConferenceParticipantSelf.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const ReactBootstrap = require('react-bootstrap');
6 | const Tooltip = ReactBootstrap.Tooltip;
7 | const OverlayTrigger = ReactBootstrap.OverlayTrigger;
8 | const sylkrtc = require('sylkrtc');
9 | const hark = require('hark');
10 | const { default: clsx } = require('clsx');
11 | const UserIcon = require('./UserIcon');
12 |
13 | class ConferenceParticipantSelf extends React.Component {
14 | constructor(props) {
15 | super(props);
16 | this.state = {
17 | active: false,
18 | hasVideo: false,
19 | sharesScreen: false
20 | }
21 | this.speechEvents = null;
22 | }
23 |
24 | componentDidMount() {
25 | sylkrtc.utils.attachMediaStream(this.props.stream, this.refs.videoElement, {disableContextMenu: true, muted: true});
26 |
27 | // factor it out to a function to avoid lint warning about calling setState here
28 | this.attachSpeechEvents();
29 | this.refs.videoElement.onresize = (event) => {
30 | this.handleResize(event)
31 | };
32 | if (this.props.audioOnly) {
33 | this.props.stream
34 | }
35 | }
36 |
37 | handleResize(event) {
38 | const resolutions = [ '1280x720', '960x540', '640x480', '640x360', '480x270','320x180'];
39 | const videoResolution = event.target.videoWidth + 'x' + event.target.videoHeight;
40 | if (resolutions.indexOf(videoResolution) === -1) {
41 | this.setState({sharesScreen: true});
42 | } else {
43 | this.setState({sharesScreen: false});
44 | }
45 | }
46 |
47 | componentWillUnmount() {
48 | if (this.speechEvents !== null) {
49 | this.speechEvents.stop();
50 | this.speechEvents = null;
51 | }
52 | }
53 |
54 | attachSpeechEvents() {
55 | this.setState({hasVideo: this.props.stream.getVideoTracks().length > 0});
56 |
57 | const options = {
58 | interval: 150,
59 | play: false
60 | };
61 | this.speechEvents = hark(this.props.stream, options);
62 | this.speechEvents.on('speaking', () => {
63 | this.setState({active: true});
64 | });
65 | this.speechEvents.on('stopped_speaking', () => {
66 | this.setState({active: false});
67 | });
68 | }
69 |
70 | render() {
71 | if (this.props.stream == null) {
72 | return false;
73 | }
74 |
75 | const tooltip = (
76 | {this.props.identity.displayName || this.props.identity.uri}
77 | );
78 |
79 | const classes = clsx({
80 | 'mirror' : this.state.hasVideo && !this.state.sharesScreen && !this.props.generatedVideoTrack,
81 | 'poster' : !this.state.hasVideo,
82 | 'fit' : this.state.hasVideo && this.state.sharesScreen,
83 | 'conference-active' : this.state.active,
84 | 'hide' : !this.state.hasVideo
85 | });
86 |
87 | let muteIcon
88 | if (this.props.audioMuted) {
89 | muteIcon = (
90 |
91 |
92 |
93 | );
94 | }
95 |
96 | return (
97 |
98 | {muteIcon}
99 |
100 |
101 | {(!this.state.hasVideo) && }
102 |
103 |
104 |
105 |
106 | );
107 | }
108 | }
109 |
110 | ConferenceParticipantSelf.propTypes = {
111 | stream: PropTypes.object.isRequired,
112 | identity: PropTypes.object.isRequired,
113 | audioMuted: PropTypes.bool.isRequired,
114 | generatedVideoTrack: PropTypes.bool,
115 | audioOnly: PropTypes.bool
116 | };
117 |
118 |
119 | module.exports = ConferenceParticipantSelf;
120 |
--------------------------------------------------------------------------------
/src/app/components/CustomContextMenu.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 |
6 | const { makeStyles } = require('@material-ui/core/styles');
7 | const { Popper, MenuList, Paper } = require('@material-ui/core');
8 | const { ClickAwayListener, Fade } = require('@material-ui/core');
9 |
10 |
11 | /* copied from https://github.com/mui-org/material-ui/blob/v4.3.2/packages/material-ui/src/Menu/Menu.js#L21 */
12 | const useMenuStyles = makeStyles({
13 | /* Styles applied to the `Paper` component. */
14 | paper: {
15 | // specZ: The maximum height of a simple menu should be one or more rows less than the view
16 | // height. This ensures a tapable area outside of the simple menu with which to dismiss
17 | // the menu.
18 | maxHeight: 'calc(100% - 96px)',
19 | // Add iOS momentum scrolling.
20 | WebkitOverflowScrolling: 'touch'
21 | },
22 | /* Styles applied to the `List` component via `MenuList`. */
23 | list: {
24 | // We disable the focus ring for mouse, touch and keyboard users.
25 | outline: 0
26 | }
27 | });
28 |
29 | const CustomContentMenu = ({anchorEl, open, children, onClose, keepMounted}) => {
30 | const menuClasses = useMenuStyles();
31 | const id = open ? 'faked-reference-popper' : undefined;
32 |
33 | return (
34 |
43 | {({ TransitionProps }) => (
44 |
45 |
46 |
47 |
48 | {children}
49 |
50 |
51 |
52 |
53 | )}
54 |
55 | );
56 | }
57 |
58 | CustomContentMenu.propTypes = {
59 | anchorEl : PropTypes.object,
60 | open : PropTypes.bool.isRequired,
61 | children : PropTypes.node.isRequired,
62 | onClose : PropTypes.func.isRequired,
63 | keepMounted : PropTypes.bool
64 | };
65 |
66 |
67 | module.exports = CustomContentMenu
68 |
--------------------------------------------------------------------------------
/src/app/components/DividerWithText.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const React = require('react');
3 | const PropTypes = require('prop-types');
4 | const { Divider, Grid } = require('@material-ui/core');
5 |
6 |
7 | const DividerWithText = ({ children }) => (
8 |
9 |
10 |
11 |
12 | {children}
13 |
14 |
15 |
16 |
17 | );
18 |
19 | DividerWithText.propTypes = {
20 | children: PropTypes.node
21 | };
22 |
23 |
24 | module.exports = DividerWithText;
25 |
--------------------------------------------------------------------------------
/src/app/components/DragAndDrop.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 |
6 |
7 | class DragAndDrop extends React.Component {
8 | state = {
9 | drag: false
10 | }
11 |
12 | dropRef = React.createRef()
13 |
14 | handleDrag = (e) => {
15 | e.preventDefault();
16 | e.stopPropagation();
17 | }
18 |
19 | handleDragIn = (e) => {
20 | e.preventDefault();
21 | e.stopPropagation();
22 | this.dragCounter++;
23 | if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
24 | this.setState({ drag: true });
25 | }
26 | }
27 |
28 | handleDragOut = (e) => {
29 | e.preventDefault();
30 | e.stopPropagation();
31 | this.dragCounter--;
32 | if (this.dragCounter === 0) {
33 | this.setState({ drag: false });
34 | }
35 | }
36 |
37 | handleDrop = (e) => {
38 | e.preventDefault();
39 | e.stopPropagation();
40 | this.setState({ drag: false });
41 | if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
42 | this.props.handleDrop(e.dataTransfer.files);
43 | e.dataTransfer.clearData();
44 | this.dragCounter = 0;
45 | }
46 | }
47 |
48 | componentDidMount() {
49 | let div = this.dropRef.current;
50 | div.addEventListener('dragenter', this.handleDragIn);
51 | div.addEventListener('dragleave', this.handleDragOut);
52 | div.addEventListener('dragover', this.handleDrag);
53 | div.addEventListener('drop', this.handleDrop);
54 | this.dragCounter = 0;
55 | }
56 |
57 | componentWillUnmount() {
58 | let div = this.dropRef.current
59 | div.removeEventListener('dragenter', this.handleDragIn);
60 | div.removeEventListener('dragleave', this.handleDragOut);
61 | div.removeEventListener('dragover', this.handleDrag);
62 | div.removeEventListener('drop', this.handleDrop);
63 | }
64 |
65 | render() {
66 | return (
67 |
71 | {
72 | this.state.drag &&
73 |
85 |
96 |
{this.props.title ? this.props.title : 'Drop files to share them to the conference'}
97 |
98 |
99 | }
100 | {this.props.children}
101 |
102 | )
103 | }
104 | }
105 |
106 | DragAndDrop.propTypes = {
107 | handleDrop: PropTypes.func.isRequired,
108 | children: PropTypes.node,
109 | title: PropTypes.string,
110 | useFlex: PropTypes.bool,
111 | small: PropTypes.bool,
112 | marginTop: PropTypes.string,
113 | style: PropTypes.object
114 | };
115 |
116 |
117 | module.exports = DragAndDrop;
118 |
--------------------------------------------------------------------------------
/src/app/components/EncryptionModal.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const React = require('react');
5 | const PropTypes = require('prop-types');
6 | const { makeStyles } = require('@material-ui/core/styles');
7 | const {
8 | Dialog,
9 | DialogTitle,
10 | DialogContent,
11 | DialogContentText,
12 | DialogActions } = require('@material-ui/core');
13 | const { Button } = require('../MaterialUIAsBootstrap');
14 |
15 |
16 | const styleSheet = makeStyles({
17 | bigger: {
18 | '&> h2': {
19 | fontSize: '20px'
20 | },
21 | '&> div > p ': {
22 | fontSize: '14px'
23 | },
24 | '&> li.MuiListSubheader-root': {
25 | fontSize: '14px',
26 | textAlign: 'left'
27 | }
28 | },
29 | fixFont: {
30 | fontFamily: 'inherit',
31 | fontSize: '14px',
32 | textAlign: 'left'
33 | },
34 | number: {
35 | fontSize: 32,
36 | textAlign: 'center',
37 | display: 'block',
38 | letterSpacing: 12,
39 | padding: 8
40 | }
41 | });
42 |
43 | function getContent(step = 0, exp) {
44 | if (step === 1 && exp) {
45 | return (
46 | To replicate messages on multiple devices you need the same private key on all of them.
47 | Press Export and enter this code when prompted on your other device:
48 | );
49 | } else if (step === 1 && !exp) {
50 | return (
51 | To replicate messages on multiple devices you need the same private key on all of them.
52 | Enter this code when prompted on your other device:
53 | );
54 | }
55 | return (
56 | You have used messaging on more than one device. To decrypt your messages, you need the same private key on all your devices
57 | To use the private key from the other device , choose the menu option 'Export private key' on that device.
58 | Do you want to keep this key ?
59 | );
60 | }
61 |
62 | // On the other devices, you'll need to enter the password that will be provided here upon export.< br/>
63 | const EncryptionModal = (props) => {
64 | const classes = styleSheet();
65 | const [step, setStep] = React.useState(0);
66 | const [password, setPassword] = React.useState();
67 |
68 | React.useEffect(() => {
69 | if (props.show === true) {
70 | setStep(0);
71 | setPassword(Math.random().toString().substr(2, 6));
72 | } else {
73 | setStep(0);
74 | setPassword('')
75 | }
76 | }, [props.show]);
77 |
78 | return (
79 |
87 |
88 | {props.export === false && step !== 1
89 | ? 'Different key detected'
90 | : 'Export private key'
91 | }
92 |
93 |
94 |
95 | {getContent(props.export ? 1 : step, props.export)}
96 | {(props.export || step === 1) && {password} }
97 |
98 |
99 |
100 | {props.export === false && step !== 1 && (
101 | {
104 | setStep(1);
105 | props.useExistingKey(password);
106 | }}
107 | title="Keep this key"
108 | >
109 | Keep this key
110 |
111 | No
112 | )}
113 | {props.export === true && (
114 | {
117 | props.exportKey(password);
118 | }}
119 | title="export"
120 | >
121 | Export
122 |
123 | )}
124 | {(step === 1 || props.export === true) && (
125 | Close
126 | )}
127 |
128 |
129 | );
130 | }
131 |
132 | EncryptionModal.propTypes = {
133 | show: PropTypes.bool.isRequired,
134 | close: PropTypes.func.isRequired,
135 | exportKey: PropTypes.func.isRequired,
136 | useExistingKey: PropTypes.func.isRequired,
137 | export: PropTypes.bool
138 | };
139 |
140 |
141 | module.exports = EncryptionModal;
142 |
--------------------------------------------------------------------------------
/src/app/components/ErrorPanel.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const ReactBootstrap = require('react-bootstrap');
6 | const Modal = ReactBootstrap.Modal;
7 |
8 |
9 | const ErrorPanel = (props) => {
10 | return (
11 |
12 |
13 | Warning
14 |
15 |
16 | {props.errorMsg}
17 |
18 |
19 | );
20 | }
21 |
22 | ErrorPanel.propTypes = {
23 | errorMsg: PropTypes.object.isRequired
24 | };
25 |
26 |
27 | module.exports = ErrorPanel;
28 |
--------------------------------------------------------------------------------
/src/app/components/EscalateConferenceModal.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const ReactBootstrap = require('react-bootstrap');
6 | const Modal = ReactBootstrap.Modal;
7 |
8 | const config = require('../config');
9 |
10 |
11 | class EscalateConferenceModal extends React.Component {
12 | constructor(props) {
13 | super(props);
14 | this.invitees = React.createRef();
15 |
16 | this.escalate = this.escalate.bind(this);
17 | }
18 |
19 | escalate(event) {
20 | event.preventDefault();
21 | const uris = [];
22 | for (let item of this.invitees.current.value.split(',')) {
23 | item = item.trim();
24 | if (item.indexOf('@') === -1) {
25 | item = `${item}@${config.defaultDomain}`;
26 | }
27 | uris.push(item);
28 | };
29 | uris.push(this.props.call.remoteIdentity.uri);
30 | this.props.escalateToConference(uris);
31 | }
32 |
33 | render() {
34 | return (
35 |
36 |
37 | Move to conference
38 |
39 |
40 | Please enter the account(s) you wish to add to this call. After pressing Move, all parties will be invited to join a conference.
41 |
52 |
53 |
54 | );
55 | }
56 | }
57 |
58 | EscalateConferenceModal.propTypes = {
59 | show: PropTypes.bool.isRequired,
60 | close: PropTypes.func.isRequired,
61 | call: PropTypes.object,
62 | escalateToConference: PropTypes.func
63 | };
64 |
65 |
66 | module.exports = EscalateConferenceModal;
67 |
--------------------------------------------------------------------------------
/src/app/components/FooterBox.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react'); // no, you can't remove this
4 |
5 |
6 | const FooterBox = () => {
7 | return (
8 |
15 | );
16 | };
17 |
18 |
19 | module.exports = FooterBox;
20 |
--------------------------------------------------------------------------------
/src/app/components/HandIcon.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 |
6 | const { makeStyles } = require('@material-ui/core/styles');
7 | const { Badge } = require('@material-ui/core');
8 |
9 |
10 | const styleSheet = makeStyles({
11 | badge: {
12 | width: '20px',
13 | height: '20px',
14 | fontWeight: 'bold',
15 | fontSize: '1rem',
16 | backgroundColor: '#337ab7'
17 | },
18 | badgeDrawer: {
19 | width: '20px',
20 | height: '20px',
21 | fontWeight: 'bold',
22 | fontSize: '1rem',
23 | backgroundColor: '#337ab7',
24 | position: 'unset',
25 | marginLeft: '-10px',
26 | transform: 'none'
27 | },
28 | rootThumb: {
29 | display: 'block',
30 | bottom: '25px',
31 | position: 'absolute',
32 | zIndex: 3
33 | }
34 | });
35 |
36 | const HandIcon = (props) => {
37 | let content = null;
38 | const classes = styleSheet();
39 | let badgeClass = classes.badge;
40 | if (props.drawer) {
41 | badgeClass = classes.badgeDrawer;
42 | }
43 | let rootClass;
44 | if (props.thumb) {
45 | rootClass = classes.rootThumb;
46 | }
47 | if (props.raisedHand !== -1) {
48 | let button = (
49 |
50 |
51 |
52 | );
53 | if (props.disableHandToggle) {
54 | button = ( );
55 | }
56 | content = (
57 |
58 | {button}
59 |
60 | );
61 | }
62 | return (content);
63 | }
64 |
65 | HandIcon.propTypes = {
66 | raisedHand : PropTypes.number.isRequired,
67 | handleHandSelected : PropTypes.func.isRequired,
68 | disableHandToggle : PropTypes.bool,
69 | drawer : PropTypes.bool,
70 | thumb : PropTypes.bool
71 | };
72 |
73 |
74 | module.exports = HandIcon;
75 |
--------------------------------------------------------------------------------
/src/app/components/HistoryCard.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const { default: clsx } = require('clsx');
6 | const { DateTime, Duration } = require('luxon');
7 |
8 | const { makeStyles } = require('@material-ui/core/styles');
9 | const { Card, CardActions, CardContent } = require('@material-ui/core');
10 | const { Typography, IconButton: Button } = require('@material-ui/core');
11 | const UserIcon = require('./UserIcon');
12 |
13 |
14 | const styles = makeStyles({
15 | card: {
16 | display: 'flex'
17 | },
18 | content: {
19 | flex: '1 0 auto',
20 | textAlign: 'left',
21 | maxWidth: 'calc( 100vw - 132px )',
22 | paddingBottom: 0
23 | },
24 | icon: {
25 | margin: 'auto'
26 | },
27 | column: {
28 | display: 'flex',
29 | flex: '1 1 auto',
30 | flexDirection: 'column',
31 | minWidth: 0
32 | },
33 | biggerFont: {
34 | fontSize: '1.1rem'
35 | },
36 | actions: {
37 | paddingTop: 4
38 | },
39 | iconSmall: {
40 | width: 40,
41 | height: 40,
42 | color: '#337ab7'
43 | },
44 | mainHeading: {
45 | color: props => props.historyItem.direction === 'received' && props.historyItem.duration === 0 ? '#a94442' : 'inherit'
46 | }
47 | });
48 |
49 | const HistoryCard = (props) => {
50 | const classes = styles(props);
51 | const identity = {
52 | displayName: props.historyItem.displayName,
53 | uri: props.historyItem.remoteParty || props.historyItem
54 | }
55 |
56 | const directionIcon = clsx({
57 | 'fa': true,
58 | 'rotate-minus-45': true,
59 | 'fa-long-arrow-left': props.historyItem.direction === 'received',
60 | 'fa-long-arrow-right': props.historyItem.direction === 'placed'
61 | });
62 |
63 | const startVideoCall = (e) => {
64 | e.stopPropagation();
65 | if (props.noConnection === false) {
66 | props.setTargetUri(identity.uri);
67 | // We need to wait for targetURI
68 | setImmediate(() => {
69 | props.startVideoCall(e);
70 | });
71 | }
72 | }
73 |
74 | const startAudioCall = (e) => {
75 | e.stopPropagation();
76 | props.setTargetUri(identity.uri);
77 | // We need to wait for targetURI
78 | setImmediate(() => {
79 | props.startAudioCall(e);
80 | });
81 | }
82 |
83 | const startChat = (e) => {
84 | e.stopPropagation();
85 | props.setTargetUri(identity.uri);
86 | // We need to wait for targetURI
87 | setImmediate(() => {
88 | props.startChat(e);
89 | });
90 | }
91 |
92 | let duration = Duration.fromObject({ seconds: props.historyItem.duration }).toFormat('hh:mm:ss');
93 | if (props.historyItem.direction === 'received' && props.historyItem.duration === 0) {
94 | duration = 'missed';
95 | }
96 |
97 | const name = identity.displayName || identity.uri;
98 |
99 | const date = DateTime.fromFormat(
100 | `${props.historyItem.startTime} ${props.historyItem.timezone}`,
101 | "yyyy-MM-dd' 'HH:mm:ss z"
102 | ).toFormat('yyyy MM dd HH:mm:ss');
103 |
104 | return (
105 | { props.setTargetUri(identity.uri) }}
108 | onDoubleClick={startVideoCall}
109 | >
110 |
111 |
112 | {name} ({duration})
113 |
114 | {date}
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | );
134 | }
135 |
136 | HistoryCard.propTypes = {
137 | historyItem: PropTypes.object,
138 | startAudioCall: PropTypes.func.isRequired,
139 | startVideoCall: PropTypes.func.isRequired,
140 | startChat: PropTypes.func.isRequired,
141 | setTargetUri: PropTypes.func.isRequired,
142 | noConnection: PropTypes.bool
143 | };
144 |
145 |
146 | module.exports = HistoryCard;
147 |
--------------------------------------------------------------------------------
/src/app/components/HistoryTileBox.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 |
6 | const { Grid } = require('@material-ui/core');
7 |
8 |
9 | const HistoryTileBox = (props) => {
10 | return (
11 |
12 |
19 | {props.children}
20 |
21 |
22 | );
23 | }
24 |
25 | HistoryTileBox.propTypes = {
26 | children: PropTypes.node
27 | };
28 |
29 |
30 | module.exports = HistoryTileBox;
31 |
--------------------------------------------------------------------------------
/src/app/components/IncomingCallModal.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const useEffect = React.useEffect;
5 | const PropTypes = require('prop-types');
6 | const ReactBootstrap = require('react-bootstrap');
7 | const Popover = ReactBootstrap.Popover;
8 | const OverlayTrigger = ReactBootstrap.OverlayTrigger;
9 |
10 | const UserIcon = require('./UserIcon');
11 |
12 |
13 | const IncomingCallModal = (props) => {
14 | useEffect(() => {
15 | document.addEventListener('keyup', onKeyUp);
16 | return (() => {
17 | document.removeEventListener('keyup', onKeyUp);
18 | });
19 | });
20 |
21 | const onKeyUp = (event) => {
22 | switch (event.which) {
23 | case 27:
24 | // ESC
25 | props.onHangup()
26 | break;
27 | default:
28 | break;
29 | }
30 | };
31 |
32 | const answerAudioOnly = () => {
33 | props.onAnswer({audio: true, video: false});
34 | }
35 |
36 | const answer = () => {
37 | props.onAnswer({audio: true, video: true});
38 | };
39 |
40 | if (props.call == null) {
41 | return false;
42 | }
43 |
44 | const buttonText = ['Decline', 'Accept', 'Audio'];
45 |
46 | let answerButtons = [
47 |
48 |
49 |
50 | {buttonText.shift()}
51 |
52 | ];
53 |
54 | let callType = 'audio';
55 | if (props.call.mediaTypes.video) {
56 | callType = 'video';
57 | answerButtons.push(
58 |
59 |
60 | {buttonText.shift()}
61 | );
62 | }
63 |
64 | answerButtons.push(
65 |
66 |
67 | {buttonText.shift()}
68 | );
69 |
70 | const remoteIdentityLine = props.call.remoteIdentity.displayName || props.call.remoteIdentity.uri;
71 |
72 | const tooltip = (
73 |
74 | {props.call.remoteIdentity.uri}
75 |
76 | );
77 |
78 | const spacers = [
79 | ,
80 |
81 | ];
82 | return (
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
{remoteIdentityLine}
92 |
is calling with {callType}
93 |
94 | {props.compact ? '' : spacers}
95 |
96 |
97 |
98 |
99 |
100 | );
101 | }
102 |
103 | IncomingCallModal.propTypes = {
104 | call : PropTypes.object,
105 | onAnswer : PropTypes.func.isRequired,
106 | onHangup : PropTypes.func.isRequired,
107 | autoFocus: PropTypes.bool.isRequired,
108 | compact : PropTypes.bool
109 | };
110 |
111 |
112 | module.exports = IncomingCallModal;
113 |
--------------------------------------------------------------------------------
/src/app/components/IncomingCallWindow.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const React = require('react');
3 | const ReactDOM = require('react-dom');
4 | const PropTypes = require('prop-types');
5 |
6 |
7 | class IncomingCallWindow extends React.Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.width = 425;
12 | this.height = 475;
13 | this.ipcRenderer = null;
14 | const { BrowserWindow } = window.require('electron').remote;
15 |
16 | this.browserWindowObject = new BrowserWindow({
17 | show: false,
18 | width: this.width,
19 | height: this.height,
20 | frame: false,
21 | skipTaskBar: true,
22 | title: 'Incoming Call',
23 | alwaysOnTop: true,
24 | backgroundColor: '#333',
25 | type: 'toolbar',
26 | webPreferences: {
27 | nodeIntegration: true,
28 | enableRemoteModule: true,
29 | contextIsolation: false
30 | }
31 | });
32 | this.el = document.createElement('div');
33 |
34 | [
35 | 'copyStyles',
36 | 'buttonClick',
37 | 'answer',
38 | 'answerAudioOnly'
39 | ].forEach((name) => {
40 | this[name] = this[name].bind(this);
41 | });
42 | }
43 |
44 | componentDidMount() {
45 | this.browserWindowObject.loadURL(`file://${window.__dirname}/incomingWindow.html`);
46 |
47 | this.browserWindowObject.once('ready-to-show', () => {
48 | if (this.props.enabled) {
49 | this.browserWindowObject.showInactive();
50 | }
51 | });
52 |
53 | this.browserWindowObject.webContents.once('dom-ready', () => {
54 | const fs = window.require('fs');
55 | const incomingWindow = fs.readFileSync(`${window.__dirname}/../incomingWindow.js`).toString('utf-8');
56 | this.browserWindowObject.webContents.executeJavaScript(incomingWindow)
57 | .then(() => {
58 | this.browserWindowObject.webContents.send('updateContent', this.el.innerHTML);
59 | this.copyStyles(document);
60 | });
61 | });
62 |
63 | this.ipcRenderer = window.require('electron').ipcRenderer;
64 | this.ipcRenderer.on('buttonClick', this.buttonClick);
65 | }
66 |
67 | componentDidUpdate(prevProps) {
68 | if (this.props.enabled !== prevProps.enabled) {
69 | if (this.props.enabled) {
70 | this.browserWindowObject.showInactive();
71 | } else {
72 | this.browserWindowObject.hide();
73 | }
74 | }
75 | }
76 |
77 | componentWillUnmount() {
78 | this.browserWindowObject.close();
79 | if (this.ipcRenderer != null) {
80 | this.ipcRenderer.removeListener('buttonClick', this.buttonClick)
81 | }
82 | }
83 |
84 | copyStyles(sourceDoc) {
85 | Array.from(sourceDoc.styleSheets).forEach(styleSheet => {
86 | if (!styleSheet.href && styleSheet.cssRules) {
87 | const newStyleEl = sourceDoc.createElement('style');
88 | Array.from(styleSheet.cssRules).forEach(cssRule => {
89 | newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
90 | });
91 | this.browserWindowObject.webContents.send('updateStyles', newStyleEl.innerHTML);
92 | }
93 | });
94 | }
95 |
96 | buttonClick(event, store) {
97 | if (store === 'audio') {
98 | this.answerAudioOnly();
99 | } else if (store === 'accept') {
100 | this.answer();
101 | } else {
102 | this.props.setFocus(false);
103 | this.props.onHangup();
104 | window.require('electron').remote.getCurrentWindow().blur();
105 | }
106 | }
107 |
108 | answerAudioOnly() {
109 | this.props.onAnswer({ audio: true, video: false });
110 | }
111 |
112 | answer() {
113 | this.props.onAnswer({ audio: true, video: true });
114 | };
115 |
116 | render() {
117 | return this.browserWindowObject ? ReactDOM.render(this.props.children, this.el) : null;
118 | }
119 | }
120 |
121 | IncomingCallWindow.propTypes = {
122 | onAnswer: PropTypes.func.isRequired,
123 | onHangup: PropTypes.func.isRequired,
124 | setFocus: PropTypes.func.isRequired,
125 | enabled: PropTypes.bool,
126 | children: PropTypes.node
127 | };
128 |
129 | module.exports = IncomingCallWindow;
130 |
--------------------------------------------------------------------------------
/src/app/components/InviteParticipantsModal.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const ReactBootstrap = require('react-bootstrap');
6 | const Modal = ReactBootstrap.Modal;
7 |
8 | const config = require('../config');
9 |
10 |
11 | class InviteParticipantsModal extends React.Component {
12 | constructor(props) {
13 | super(props);
14 | this.invitees = React.createRef();
15 |
16 | this.invite = this.invite.bind(this);
17 | }
18 |
19 | invite(event) {
20 | event.preventDefault();
21 | const uris = [];
22 | this.invitees.current.value.split(',').forEach((item) => {
23 | item = item.trim();
24 | if (item.indexOf('@') === -1) {
25 | item = `${item}@${config.defaultDomain}`;
26 | }
27 | uris.push(item);
28 | });
29 | if (uris && this.props.call) {
30 | this.props.call.inviteParticipants(uris);
31 | }
32 | this.props.close();
33 | }
34 |
35 | render() {
36 | return (
37 |
38 |
39 | Invite Online Users
40 |
41 |
42 | Enter the users you wish to invite
43 |
54 |
55 |
56 | );
57 | }
58 | }
59 |
60 | InviteParticipantsModal.propTypes = {
61 | show: PropTypes.bool.isRequired,
62 | close: PropTypes.func.isRequired,
63 | call: PropTypes.object
64 | };
65 |
66 |
67 | module.exports = InviteParticipantsModal;
68 |
--------------------------------------------------------------------------------
/src/app/components/ListWithStickyHeader.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 |
6 | const { useInView } = require('react-intersection-observer');
7 |
8 | const { makeStyles } = require('@material-ui/core/styles');
9 |
10 |
11 | const styleSheet = makeStyles((theme) => ({
12 | sticky: {
13 | position: 'sticky',
14 | top: '-1px',
15 | backgroundColor: 'rgba(230, 230, 230, .85)',
16 | zIndex: 1
17 | }
18 | }));
19 |
20 | const ListWithStickyHeader = ({ children, header }) => {
21 | const classes = styleSheet();
22 |
23 | const { ref, inView } = useInView({
24 | threshold: 0
25 | });
26 |
27 | const { ref: ref2, inView: inView2 } = useInView({
28 | threshold: [1]
29 | });
30 | return (
31 |
32 | {header}
33 | {children}
34 |
35 | )
36 | }
37 |
38 | ListWithStickyHeader.propTypes = {
39 | header: PropTypes.object,
40 | children: PropTypes.object
41 | };
42 |
43 |
44 | module.exports = ListWithStickyHeader;
45 |
--------------------------------------------------------------------------------
/src/app/components/LoadingScreen.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const { default: clsx } = require('clsx');
6 |
7 | const LoadingScreen = (props) => {
8 | const textDisplayClasses = clsx({
9 | 'hidden': props.text.length === 0
10 | });
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
{props.text}
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | LoadingScreen.propTypes = {
27 | text: PropTypes.string
28 | };
29 |
30 | module.exports = LoadingScreen;
31 |
--------------------------------------------------------------------------------
/src/app/components/LocalMedia.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const sylkrtc = require('sylkrtc');
6 | const { default: clsx } = require('clsx');
7 |
8 | const CallOverlay = require('./CallOverlay');
9 |
10 |
11 | class LocalMedia extends React.Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | this.localVideo = React.createRef();
16 |
17 | // ES6 classes no longer autobind
18 | this.hangupCall = this.hangupCall.bind(this);
19 | this.localVideoElementPlaying = this.localVideoElementPlaying.bind(this);
20 | }
21 |
22 | componentDidMount() {
23 | this.localVideo.current.addEventListener('playing', this.localVideoElementPlaying);
24 | sylkrtc.utils.attachMediaStream(this.props.localMedia, this.localVideo.current, {disableContextMenu: true, muted: true});
25 | }
26 |
27 | componentWillUnmount() {
28 | this.localVideo.current.removeEventListener('playing', this.localVideoElementPlaying);
29 | }
30 |
31 | localVideoElementPlaying() {
32 | this.localVideo.current.removeEventListener('playing', this.localVideoElementPlaying);
33 | this.props.mediaPlaying();
34 | }
35 |
36 | hangupCall(event) {
37 | event.preventDefault();
38 | this.props.hangupCall();
39 | }
40 |
41 | render() {
42 | const localVideoClasses = clsx({
43 | 'large' : true,
44 | 'animated' : true,
45 | 'fadeIn' : true,
46 | 'mirror' : !this.props.generatedVideoTrack
47 | });
48 |
49 | return (
50 |
51 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | }
63 | }
64 |
65 | LocalMedia.propTypes = {
66 | hangupCall : PropTypes.func,
67 | localMedia : PropTypes.object.isRequired,
68 | mediaPlaying : PropTypes.func.isRequired,
69 | remoteIdentity : PropTypes.string,
70 | generatedVideoTrack : PropTypes.bool
71 | };
72 |
73 |
74 | module.exports = LocalMedia;
75 |
--------------------------------------------------------------------------------
/src/app/components/Logo.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 |
5 |
6 | const Logo = () => {
7 | return (
8 |
12 | );
13 | }
14 |
15 |
16 | module.exports = Logo;
17 |
--------------------------------------------------------------------------------
/src/app/components/LogoutModal.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const React = require('react');
5 | const PropTypes = require('prop-types');
6 | const { makeStyles } = require('@material-ui/core/styles');
7 | const {
8 | Checkbox,
9 | Dialog,
10 | DialogActions,
11 | DialogContent,
12 | DialogContentText,
13 | DialogTitle,
14 | IconButton,
15 | FormGroup,
16 | FormControlLabel } = require('@material-ui/core');
17 | const {
18 | Close: CloseIcon } = require('@material-ui/icons');
19 | const { Button } = require('../MaterialUIAsBootstrap');
20 |
21 |
22 | const styleSheet = makeStyles((theme) => ({
23 | bigger: {
24 | '&> h2': {
25 | fontSize: '20px'
26 | },
27 | '&> div > p ': {
28 | fontSize: '14px'
29 | },
30 | '&> li.MuiListSubheader-root': {
31 | fontSize: '14px',
32 | textAlign: 'left'
33 | }
34 | },
35 | root: {
36 | marginTop: -5,
37 | '&$checked': {
38 | },
39 | '& .MuiSvgIcon-root': {
40 | fontSize: 24
41 | }
42 | },
43 | checked: {},
44 | center: {
45 | flexGrow: 1,
46 | paddingLeft: 12,
47 | color: 'rgba(0, 0, 0, 0.54)',
48 | marginBottom: 0
49 | },
50 | fixFont: {
51 | fontFamily: 'inherit',
52 | fontSize: '14px',
53 | textAlign: 'left'
54 | },
55 | fixFontCheck: {
56 | marginBottom: 0
57 | },
58 | closeButton: {
59 | position: 'absolute',
60 | right: theme.spacing(1),
61 | top: theme.spacing(1),
62 | color: theme.palette.grey[500],
63 | '&> span > svg': {
64 | fontSize: 24
65 | }
66 | }
67 | }));
68 |
69 |
70 | const LogoutModal = (props) => {
71 | const classes = styleSheet();
72 | const [removeData, _setRemoveData] = React.useState(false);
73 |
74 | const setRemoveData = (event) => {
75 | _setRemoveData(event.target.checked);
76 | }
77 |
78 | return (
79 | {
82 | if (reason !== 'backdropClick') {
83 | props.close();
84 | }
85 | }}
86 | maxWidth="sm"
87 | fullWidth={true}
88 | aria-labelledby="dialog-titile"
89 | aria-describedby="dialog-description"
90 | disableEscapeKeyDown
91 | >
92 |
93 | Sign out of Sylk
94 |
95 |
96 |
97 |
98 |
99 |
100 | You will be no longer reachable for calls and messages on this device/browser.
101 |
102 |
103 |
104 |
110 |
124 | }
125 | label="Also remove your existing data"
126 | />
127 |
128 | Cancel
129 | {props.logout(removeData); _setRemoveData(false)}} variant="contained" title="Sign Out">Sign Out
130 |
131 |
132 | );
133 | }
134 |
135 | LogoutModal.propTypes = {
136 | show: PropTypes.bool.isRequired,
137 | close: PropTypes.func.isRequired,
138 | logout: PropTypes.func.isRequired
139 | };
140 |
141 |
142 | module.exports = LogoutModal;
143 |
--------------------------------------------------------------------------------
/src/app/components/MessagesLoadingScreen.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 |
6 | const { LinearProgress } = require('@material-ui/core');
7 |
8 |
9 | const MessagesLoadingScreen = (props) => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 | {props.progress !== 'storing'
18 | ?
Decrypting messages ...
19 | :
Processing messages ...
20 | }
21 |
22 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | MessagesLoadingScreen.propTypes = {
38 | progress: PropTypes.any.isRequired
39 | };
40 |
41 |
42 | module.exports = MessagesLoadingScreen;
43 |
--------------------------------------------------------------------------------
/src/app/components/MuteAudioParticipantsModal.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const ReactBootstrap = require('react-bootstrap');
6 | const Modal = ReactBootstrap.Modal;
7 |
8 |
9 | const MuteAudioParticipantsModal = (props) => {
10 | const handleMute = () => {
11 | props.handleMute();
12 | props.close();
13 | }
14 | return (
15 |
16 |
17 | Mute audio from everybody except yourself?
18 |
19 |
20 | You can mute the audio from everybody, but you can't unmute them.
21 | They can unmute themselves at any time.
22 |
23 | Mute
24 | Cancel
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | MuteAudioParticipantsModal.propTypes = {
32 | show: PropTypes.bool.isRequired,
33 | close: PropTypes.func.isRequired,
34 | handleMute: PropTypes.func.isRequired
35 | };
36 |
37 |
38 | module.exports = MuteAudioParticipantsModal;
39 |
--------------------------------------------------------------------------------
/src/app/components/NewDeviceModal.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const React = require('react');
5 | const PropTypes = require('prop-types');
6 | const { makeStyles } = require('@material-ui/core/styles');
7 | const {
8 | Dialog,
9 | DialogTitle,
10 | DialogContent,
11 | DialogContentText,
12 | DialogActions } = require('@material-ui/core');
13 | const { Button } = require('../MaterialUIAsBootstrap');
14 |
15 |
16 | const styleSheet = makeStyles({
17 | bigger: {
18 | '&> h2': {
19 | fontSize: '20px'
20 | },
21 | '&> div > p ': {
22 | fontSize: '14px'
23 | },
24 | '&> li.MuiListSubheader-root': {
25 | fontSize: '14px',
26 | textAlign: 'left'
27 | }
28 | },
29 | fixFont: {
30 | fontFamily: 'inherit',
31 | fontSize: '14px',
32 | textAlign: 'left'
33 | }
34 | });
35 |
36 | function getContent() {
37 | return (
38 | To decrypt your messages, your private key is required.
39 | Please choose 'Export private key' on a device/browser where you signed in before.
40 | If you lost access to this device/browser, please continue with 'Generate a new private key',
41 | or 'Cancel' and messaging will be disabled .
42 | If you choose to generate a new key, your previous messages cannot be read on newer devices.
43 | );
44 | }
45 |
46 | const NewDeviceModal = (props) => {
47 | const classes = styleSheet();
48 |
49 | return (
50 | {
53 | if (reason !== 'backdropClick') {
54 | props.close();
55 | }
56 | }}
57 | maxWidth="sm"
58 | fullWidth={true}
59 | aria-labelledby="dialog-titile"
60 | aria-describedby="dialog-description"
61 | disableEscapeKeyDown
62 | >
63 | New device/browser?
64 |
65 |
66 | {getContent()}
67 |
68 |
69 |
70 | Cancel
71 |
77 | Generate Private Key
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | NewDeviceModal.propTypes = {
85 | show: PropTypes.bool.isRequired,
86 | close: PropTypes.func.isRequired,
87 | generatePGPKeys: PropTypes.func.isRequired,
88 | private: PropTypes.bool.isRequired
89 | };
90 |
91 |
92 | module.exports = NewDeviceModal;
93 |
--------------------------------------------------------------------------------
/src/app/components/PreMedia.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const useRef = React.useRef;
5 | const useEffect = React.useEffect;
6 | const useState = React.useState;
7 | const PropTypes = require('prop-types');
8 | const { default: clsx } = require('clsx');
9 |
10 | const { default: CSSTransition } = require('react-transition-group/CSSTransition');
11 |
12 | const { makeStyles } = require('@material-ui/core/styles');
13 |
14 | const sylkrtc = require('sylkrtc');
15 |
16 |
17 | const styleSheet = makeStyles({
18 | premediaOverlay: {
19 | position: 'absolute',
20 | top: 0,
21 | left: 0,
22 | width: '100%',
23 | height: '100%',
24 | background: 'linear-gradient(transparent, rgba(0,0,0,.9))'
25 | },
26 | hide: {
27 | opacity: 0,
28 | visibility: 'hidden'
29 | },
30 | background: {
31 | zIndex: -1
32 | }
33 | });
34 |
35 | const PreMedia = (props) => {
36 | const classes = styleSheet(props);
37 | const [show, setShow] = useState(false);
38 | const [init, setInit] = useState(false);
39 | const localVideo = useRef(null);
40 |
41 | useEffect(() => {
42 | if (localVideo.current !== null && props.localMedia) {
43 | if (props.localMedia.getVideoTracks().length !== 0) {
44 | localVideo.current.addEventListener('playing', localVideoElementPlaying);
45 | sylkrtc.utils.attachMediaStream(props.localMedia, localVideo.current, {disableContextMenu: true, muted: true});
46 | }
47 | }
48 | return (() => {
49 | if (localVideo.current !== null) {
50 | localVideo.current.removeEventListener('playing', localVideoElementPlaying); //eslint-disable-line react-hooks/exhaustive-deps
51 | }
52 | })
53 | }, [props.localMedia]);
54 |
55 | useEffect(() => {
56 | if (localVideo.current !== null && props.hide) {
57 | localVideo.current.removeEventListener('playing', localVideoElementPlaying);
58 | setShow(false);
59 | }
60 | }, [props.hide]);
61 |
62 | const enter = () => {
63 | if (!init) {
64 | setInit(true);
65 | }
66 | };
67 |
68 | const localVideoElementPlaying = () => {
69 | setShow(true);
70 | };
71 |
72 | const videoClasses = clsx({
73 | 'video-container' : true
74 | },
75 | !init && classes.hide,
76 | classes.background
77 | );
78 |
79 | return (
80 |
81 | {props.localMedia &&
82 |
88 |
92 |
93 | }
94 |
95 | );
96 | }
97 |
98 | PreMedia.propTypes = {
99 | localMedia: PropTypes.object,
100 | hide: PropTypes.bool
101 | };
102 |
103 |
104 | module.exports = PreMedia;
105 |
--------------------------------------------------------------------------------
/src/app/components/RedialScreen.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const useState = React.useState;
5 | const useEffect = React.useEffect;
6 | const useRef = React.useRef;
7 | const PropTypes = require('prop-types');
8 |
9 | const { LinearProgress } = require('@material-ui/core');
10 |
11 |
12 | const useInterval = (callback, delay) => {
13 | const savedCallback = useRef();
14 |
15 | // Remember the latest callback.
16 | useEffect(() => {
17 | savedCallback.current = callback;
18 | }, [callback]);
19 |
20 | // Set up the interval.
21 | useEffect(() => {
22 | function tick() {
23 | savedCallback.current();
24 | }
25 | if (delay !== null) {
26 | let id = setInterval(tick, delay);
27 | return () => clearInterval(id);
28 | }
29 | }, [delay]);
30 | }
31 |
32 | const RedialScreen = (props) => {
33 | let [progress, setProgress] = useState(0);
34 |
35 | let retryTime = 60;
36 | if (props.router.getPath().startsWith('/conference')) {
37 | retryTime = 180;
38 | }
39 | let interval = 500;
40 |
41 | useInterval(() => {
42 | progress = progress + .5
43 |
44 | if (progress !== retryTime) {
45 | setProgress(progress);
46 | } else {
47 | hide();
48 | }
49 | }, interval);
50 |
51 | const hide = () => {
52 | props.hide();
53 | props.router.navigate('/ready');
54 | }
55 |
56 | return (
57 |
58 |
59 |
60 |
61 |
62 |
63 |
Please wait...
64 |
The connection has been lost. Attempting to resume the call.
65 |
66 |
72 |
73 | Cancel resume
74 |
75 |
76 |
77 |
78 |
79 | );
80 | }
81 |
82 | RedialScreen.propTypes = {
83 | router: PropTypes.object.isRequired,
84 | hide: PropTypes.func.isRequired
85 | };
86 |
87 |
88 | module.exports = RedialScreen;
89 |
--------------------------------------------------------------------------------
/src/app/components/RegisterBox.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 |
6 | const RegisterForm = require('./RegisterForm');
7 | const Logo = require('./Logo');
8 |
9 |
10 | const RegisterBox = (props) => {
11 | return (
12 |
22 | );
23 | };
24 |
25 | RegisterBox.propTypes = {
26 | handleRegistration : PropTypes.func.isRequired,
27 | registrationInProgress : PropTypes.bool,
28 | autoLogin : PropTypes.bool
29 | };
30 |
31 |
32 | module.exports = RegisterBox;
33 |
--------------------------------------------------------------------------------
/src/app/components/ShortcutsModal.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const React = require('react');
5 | const PropTypes = require('prop-types');
6 | const { makeStyles } = require('@material-ui/core/styles');
7 | const { Dialog, DialogTitle, DialogContent, DialogActions, Divider } = require('@material-ui/core');
8 | const { List, ListSubheader, ListItem, ListItemText, ListItemSecondaryAction } = require('@material-ui/core');
9 |
10 | const { Button } = require('../MaterialUIAsBootstrap');
11 |
12 | const styleSheet = makeStyles({
13 | bigger: {
14 | '&> h2': {
15 | fontSize: '20px'
16 | },
17 | '&> li > div > div > span ': {
18 | fontSize: '14px'
19 | },
20 | '&> li.MuiListSubheader-root': {
21 | fontSize: '14px',
22 | textAlign: 'left'
23 | }
24 | }
25 | });
26 |
27 | const ShortcutsModal = (props) => {
28 | const classes = styleSheet();
29 | return (
30 |
37 | Keyboard Shortcuts
38 |
39 |
40 |
41 | Mute or unmute your microphone
42 | M
43 |
44 |
45 | Mute or unmute your video
46 | V
47 |
48 |
49 | View or exit full screen
50 | F
51 |
52 |
53 | Switch between camera and screen sharing
54 | S
55 |
56 |
57 |
58 | Conferences:
59 |
60 |
61 | Open or close the chat
62 | C
63 |
64 |
65 | Open or close dialog to switch devices
66 | D
67 |
68 |
69 | Cycle through active speakers
70 | space
71 |
72 |
73 | Raise or lower your hand
74 | H
75 |
76 |
77 |
78 | Show help
79 | ?
80 |
81 |
82 |
83 |
84 |
85 | Close
86 |
87 |
88 |
89 | );
90 | }
91 |
92 | ShortcutsModal.propTypes = {
93 | show: PropTypes.bool.isRequired,
94 | close: PropTypes.func.isRequired
95 | };
96 |
97 |
98 | module.exports = ShortcutsModal;
99 |
--------------------------------------------------------------------------------
/src/app/components/Statistics.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const React = require('react');
5 | const PropTypes = require('prop-types');
6 |
7 | const { Box } = require('@material-ui/core');
8 |
9 | const {Tab, Tabs} = require('../MaterialUIAsBootstrap');
10 | const Charts = require('./Statistics/Charts');
11 |
12 | /* eslint-disable react/no-multi-comp */
13 | const TabPanel = (props) => {
14 | const { children, value, index } = props;
15 | return (
16 |
22 | {value === index && (
23 | {children}
24 | )}
25 |
26 | );
27 | }
28 |
29 | TabPanel.propTypes = {
30 | children: PropTypes.node,
31 | index: PropTypes.any.isRequired,
32 | value: PropTypes.any.isRequired
33 | };
34 |
35 | function a11yProps(index) {
36 | return {
37 | id: `tab-${index}`,
38 | 'aria-controls': `tabpanel-${index}`
39 | };
40 | }
41 |
42 | const Statistics = ({
43 | videoData,
44 | audioData,
45 | lastData,
46 | videoElements,
47 | video,
48 | details
49 | }) => {
50 | const [value, setValue] = React.useState(0);
51 | const videoGraphs = video !== undefined && video !== false;
52 |
53 | const handleChange = (event, newValue) => {
54 | setValue(newValue);
55 | };
56 |
57 | return (
58 |
59 | {videoGraphs ?
60 |
61 |
70 |
71 |
72 |
73 |
74 |
81 |
82 |
83 |
87 |
88 |
89 | :
90 |
94 | }
95 |
96 | )
97 | };
98 |
99 | Statistics.propTypes = {
100 | videoData: PropTypes.array,
101 | audioData: PropTypes.array,
102 | lastData: PropTypes.object,
103 | videoElements: PropTypes.object,
104 | video: PropTypes.bool,
105 | details: PropTypes.bool
106 | };
107 |
108 |
109 | module.exports = Statistics;
110 |
111 |
--------------------------------------------------------------------------------
/src/app/components/Statistics/AreaGradientChart.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const {
6 | ResponsiveContainer,
7 | AreaChart,
8 | Area } = require('recharts');
9 |
10 |
11 | const AreaGradientGraph = ({
12 | data,
13 | dataKey,
14 | height,
15 | color
16 | }) => {
17 | let stroke = '#2e6da4';
18 | let fill = 'url(#colorBlue)';
19 |
20 | if (color && color === 'green') {
21 | fill = 'url(#colorGreen)';
22 | stroke = '#4cae4c';
23 | }
24 | if (!height) {
25 | height = 60
26 | }
27 |
28 | return (
29 |
30 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
54 |
55 |
56 | )
57 | };
58 |
59 | AreaGradientGraph.propTypes = {
60 | data: PropTypes.array.isRequired,
61 | dataKey: PropTypes.string.isRequired,
62 | height: PropTypes.number,
63 | color: PropTypes.string
64 | };
65 |
66 |
67 | module.exports = AreaGradientGraph;
68 |
69 |
--------------------------------------------------------------------------------
/src/app/components/Statistics/LineChart.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 |
6 | const {
7 | ResponsiveContainer,
8 | LineChart,
9 | Line } = require('recharts');
10 |
11 | const SylkLineChart = ({
12 | data,
13 | dataKey,
14 | height,
15 | type
16 | }) => {
17 | if (!height) {
18 | height = 60
19 | }
20 | if (!type) {
21 | type = 'step';
22 | }
23 |
24 | return (
25 |
26 |
32 |
39 |
40 |
41 | )
42 | };
43 |
44 | SylkLineChart.propTypes = {
45 | data: PropTypes.array.isRequired,
46 | dataKey: PropTypes.string.isRequired,
47 | height: PropTypes.number,
48 | type: PropTypes.string
49 | };
50 |
51 |
52 | module.exports = SylkLineChart;
53 |
54 |
--------------------------------------------------------------------------------
/src/app/components/StatusBox.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const { default: clsx } = require('clsx');
6 |
7 |
8 | const StatusBox = (props) => {
9 | const classes = clsx({
10 | 'alert' : true,
11 | 'alert-warning' : props.level === 'warning',
12 | 'alert-danger' : props.level === 'danger',
13 | 'alert-info' : props.level === 'info'
14 | });
15 |
16 | const widthClasses = clsx({
17 | 'form-signin' : props.width === 'small' || !props.width,
18 | 'form-dial' : props.width === 'medium',
19 | 'half-width' : props.width === 'large'
20 | });
21 |
22 | let message;
23 | if (props.title) {
24 | message = ({props.title} {props.message}
);
25 | } else {
26 | message = ({props.message}
);
27 | }
28 |
29 | return (
30 |
31 |
32 | {message}
33 |
34 |
35 | );
36 | };
37 |
38 | StatusBox.propTypes = {
39 | level: PropTypes.string,
40 | message: PropTypes.string.isRequired,
41 | title: PropTypes.string,
42 | width: PropTypes.oneOf(['small', 'medium','large'])
43 | };
44 |
45 |
46 | module.exports = StatusBox;
47 |
--------------------------------------------------------------------------------
/src/app/components/SwitchDevicesMenu/AudioMenuItem.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const { useRef } = React;
5 | const PropTypes = require('prop-types');
6 | const { makeStyles } = require('@material-ui/core/styles');
7 | const { ListItemIcon } = require('@material-ui/core');
8 | const { default: clsx } = require('clsx');
9 | const sylkrtc = require('sylkrtc');
10 |
11 | const VolumeBar = require('../VolumeBar')
12 |
13 |
14 | const styleSheet = makeStyles((theme) => ({
15 | audioLabel: {
16 | overflow: 'hidden',
17 | textOverflow: 'ellipsis',
18 | paddingLeft: '20px',
19 | flex: 3
20 | },
21 | audioLabelSelected: {
22 | paddingLeft: 0
23 | },
24 | icon: {
25 | minWidth: '20px'
26 | }
27 | }));
28 |
29 | const AudioMenuItem = (props) => {
30 | const classes = styleSheet();
31 | const volume = useRef();
32 |
33 | const failed = () => {
34 | return props.stream && props.stream === 'failed'
35 | }
36 |
37 | return (
38 |
39 | {props.selected &&
40 |
41 |
42 |
43 | }
44 | {props.label}
45 | {props.stream && !failed() &&
46 |
47 | }
48 |
49 | );
50 | }
51 |
52 | AudioMenuItem.propTypes = {
53 | stream: PropTypes.any,
54 | selected: PropTypes.bool,
55 | label: PropTypes.string
56 | };
57 |
58 |
59 | module.exports = AudioMenuItem;
60 |
--------------------------------------------------------------------------------
/src/app/components/SwitchDevicesMenu/VideoMenuItem.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const {useEffect, useRef} = React;
5 | const PropTypes = require('prop-types');
6 | const { makeStyles } = require('@material-ui/core/styles');
7 | const { Fade, CircularProgress } = require('@material-ui/core');
8 |
9 | const sylkrtc = require('sylkrtc');
10 |
11 | const { default: clsx } = require('clsx');
12 |
13 | const styleSheet = makeStyles((theme) => ({
14 | menuVideoContainer: {
15 | width: '240px',
16 | height: '132px',
17 | position: 'relative'
18 | },
19 | scaleFit: {
20 | transform: 'scale(-1, 1) !important',
21 | width: '100%',
22 | height: '100%',
23 | objectFit: 'cover'
24 | },
25 | videoOverlay: {
26 | position: 'absolute',
27 | width: '100%',
28 | height: '100%',
29 | background: 'rgba(50,50,50,.6)',
30 | zIndex: 1
31 | },
32 | videoLabel: {
33 | width: '220px',
34 | fontSize: 14,
35 | zIndex: 2,
36 | position: 'absolute',
37 | color: '#fff',
38 | textAlign: 'center',
39 | padding: '8px',
40 | textOverflow: 'ellipsis'
41 | },
42 | pending: {
43 | alignItems: 'center',
44 | display: 'flex',
45 | height: '100%',
46 | justifyContent: 'center',
47 | position: 'absolute',
48 | width: '100%',
49 | zIndex: 2
50 | },
51 | failedText: {
52 | fontSize: 14
53 | },
54 | selectedBorder: {
55 | border: '3px solid #4cae4c'
56 | }
57 | }));
58 |
59 | const VideoMenuItem = (props) => {
60 | const classes = styleSheet();
61 | const video = useRef();
62 |
63 | useEffect(() => {
64 | if (props.stream !== 'failed') {
65 | sylkrtc.utils.attachMediaStream(props.stream, video.current, {muted: true, disableContextMenu: true, mirror: true});
66 | }
67 | }, [props.stream])
68 |
69 | const failed = () => {
70 | return props.stream && props.stream === 'failed'
71 | }
72 |
73 | return (
74 |
75 | { failed() &&
76 |
Camera unavailable
77 | }
78 | { props.stream && !failed() &&
79 |
{props.label}
80 | }
81 | { props.stream ? (
82 |
83 |
84 |
85 |
86 | ) : (
87 |
88 |
96 |
97 |
98 |
99 | )}
100 |
101 | );
102 | }
103 |
104 | VideoMenuItem.propTypes = {
105 | stream: PropTypes.any,
106 | selected: PropTypes.bool,
107 | label: PropTypes.string
108 | };
109 |
110 |
111 | module.exports = VideoMenuItem;
112 |
--------------------------------------------------------------------------------
/src/app/components/TabPanel.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 |
6 |
7 | const TabPanel = (props) => {
8 | const { children, value, index, ...other } = props;
9 | return (
10 |
17 | {value === index && (
18 |
19 | {children}
20 |
21 | )}
22 |
23 | );
24 | }
25 |
26 |
27 | TabPanel.propTypes = {
28 | children: PropTypes.node,
29 | index: PropTypes.any.isRequired,
30 | value: PropTypes.any.isRequired
31 | };
32 |
33 | module.exports = TabPanel;
34 |
--------------------------------------------------------------------------------
/src/app/components/URIInput.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const autocomplete = require('autocomplete.js');
6 |
7 |
8 | class URIInput extends React.Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | selecting: false
13 | };
14 |
15 | this.uriInput = React.createRef();
16 |
17 | // ES6 classes no longer autobind
18 | this.onInputBlur = this.onInputBlur.bind(this);
19 | this.onInputChange = this.onInputChange.bind(this);
20 | this.onInputKeyDown = this.onInputKeyDown.bind(this);
21 | this.onInputClick = this.onInputClick.bind(this);
22 | this.clicked = false;
23 | this.autoComplete;
24 | }
25 |
26 | componentDidMount() {
27 | this.autoComplete = autocomplete('#uri-input', { hint: false }, [
28 | {
29 | source: (query, cb) => {
30 | let data = this.props.data.filter((item) => {
31 | return item.startsWith(query);
32 | });
33 | cb(data);
34 | },
35 | displayKey: String,
36 | templates: {
37 | suggestion: (suggestion) => {
38 | return suggestion;
39 | }
40 | }
41 | }
42 | ]).on('autocomplete:selected', (event, suggestion, dataset) => {
43 | this.setValue(suggestion);
44 | });
45 |
46 | if (this.props.autoFocus) {
47 | this.uriInput.current.focus();
48 | }
49 | }
50 |
51 | componentDidUpdate(prevProps) {
52 | if (prevProps.defaultValue !== this.props.defaultValue && this.props.autoFocus) {
53 | this.uriInput.current.focus();
54 | }
55 | }
56 |
57 | setValue(value) {
58 | this.props.onChange(value);
59 | }
60 |
61 | onInputChange(event) {
62 | this.setValue(event.target.value);
63 | }
64 |
65 | onInputClick(event) {
66 | if (!this.clicked) {
67 | this.uriInput.current.select();
68 | this.clicked = true;
69 | }
70 | }
71 |
72 | onInputKeyDown(event) {
73 | switch (event.which) {
74 | case 13:
75 | // ENTER
76 | if (this.state.selecting) {
77 | this.setState({selecting: false});
78 | } else {
79 | this.props.onSelect(event.target.value);
80 | }
81 | break;
82 | case 27:
83 | // ESC
84 | this.setState({selecting: false});
85 | break;
86 | case 38:
87 | case 40:
88 | // UP / DOWN ARROW
89 | this.setState({selecting: true});
90 | break;
91 | default:
92 | break;
93 | }
94 | }
95 |
96 | onInputBlur(event) {
97 | // focus was lost, reset selecting state
98 | if (this.state.selecting) {
99 | this.setState({selecting: false});
100 | }
101 | this.clicked = false;
102 | }
103 |
104 | render() {
105 | return (
106 |
107 |
119 |
120 | );
121 |
122 | }
123 | }
124 |
125 | URIInput.propTypes = {
126 | defaultValue: PropTypes.string.isRequired,
127 | data: PropTypes.array.isRequired,
128 | autoFocus: PropTypes.bool.isRequired,
129 | onChange: PropTypes.func.isRequired,
130 | onSelect: PropTypes.func.isRequired,
131 | placeholder : PropTypes.string
132 | };
133 |
134 |
135 | module.exports = URIInput;
136 |
--------------------------------------------------------------------------------
/src/app/components/UserIcon.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const utils = require('../utils');
6 |
7 | const { makeStyles } = require('@material-ui/core/styles');
8 | const { Avatar } = require('@material-ui/core');
9 |
10 | const { default: clsx } = require('clsx');
11 |
12 |
13 | const styleSheet = makeStyles({
14 | root: {
15 | transition: 'box-shadow 0.3s'
16 | },
17 | drawerAvatar: {
18 | fontFamily: 'Helvetica Neue ,Helvetica, Arial, sans-serif',
19 | textTransform: 'uppercase'
20 | },
21 | margin: {
22 | margin: '5px'
23 | },
24 | card: {
25 | width: '70px',
26 | height: '70px',
27 | fontSize: '2.5rem',
28 | margin: '10px'
29 | },
30 | chatContact: {
31 | width: '50px',
32 | height: '50px',
33 | fontSize: '1.5625rem',
34 | margin: 0
35 | },
36 | carousel: {
37 | width: '80px',
38 | height: '80px',
39 | fontSize: '2.85rem',
40 | margin: 'auto'
41 | },
42 | large: {
43 | width: '144px',
44 | height: '144px',
45 | fontSize: '5rem',
46 | margin: 'auto'
47 | },
48 | shadow: {
49 | boxShadow: '0 0 2px 2px #999'
50 | },
51 | shadowSmall: {
52 | boxShadow: '0 0 5px 2px #999'
53 | }
54 | });
55 |
56 | const UserIcon = (props) => {
57 | const classes = styleSheet();
58 | const name = props.identity.displayName || props.identity.uri;
59 | let initials = name.split(' ', 2).map(x => x[0]).join('');
60 | const color = utils.generateMaterialColor(props.identity.uri)['300'];
61 | const avatarClasses = clsx(
62 | classes.root,
63 | classes.drawerAvatar,
64 | {[`${classes.card}`]: props.card},
65 | {[`${classes.chatContact}`]: props.chatContact},
66 | {[`${classes.large}`]: props.large},
67 | {[`${classes.shadow}`]: props.active},
68 | {[`${classes.shadowSmall}`]: props.active && props.small},
69 | {[`${classes.carousel}`]: props.carousel},
70 | {[`${classes.margin}`]: props.small}
71 | );
72 |
73 | if (props.identity.uri === 'anonymous@anonymous.invalid') {
74 | initials = ;
75 | }
76 |
77 | return (
78 |
79 | {initials}
80 |
81 | );
82 | };
83 |
84 | UserIcon.propTypes = {
85 | identity: PropTypes.object.isRequired,
86 | large: PropTypes.bool,
87 | card: PropTypes.bool,
88 | carousel: PropTypes.bool,
89 | small: PropTypes.bool,
90 | active: PropTypes.bool,
91 | chatContact: PropTypes.bool
92 | };
93 |
94 |
95 | module.exports = UserIcon;
96 |
--------------------------------------------------------------------------------
/src/app/components/VolumeBar.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const PropTypes = require('prop-types');
5 | const hark = require('hark');
6 |
7 | const { withStyles } = require('@material-ui/core/styles');
8 | const { green } = require('@material-ui/core/colors');
9 | const { LinearProgress } = require('@material-ui/core');
10 |
11 |
12 | const styleSheet = {
13 | colorSecondary: {
14 | backgroundColor: green[100]
15 | },
16 | barColorSecondary: {
17 | backgroundColor: green[500]
18 | },
19 | root: {
20 | height: '10px',
21 | opacity: '0.7'
22 | },
23 | bar1Determinate: {
24 | transition: 'transform 0.2s linear'
25 | }
26 | };
27 |
28 | class VolumeBar extends React.Component {
29 |
30 | constructor(props) {
31 | super(props);
32 | this.speechEvents = null;
33 | this.state = {
34 | volume: 0
35 | }
36 | }
37 |
38 | componentDidMount() {
39 | const options = {
40 | interval: 225,
41 | play: false
42 | };
43 | this.speechEvents = hark(this.props.localMedia, options);
44 | this.speechEvents.on('volume_change', (vol, threshold) => {
45 | this.setState({volume: 2 * (vol + 75)});
46 | });
47 | }
48 |
49 | componentDidUpdate(prevProps) {
50 | if (prevProps.localMedia !== this.props.localMedia) {
51 | if (this.speechEvents !== null) {
52 | this.speechEvents.stop();
53 | this.speechEvents = null;
54 | }
55 | const options = {
56 | interval: 225,
57 | play: false
58 | };
59 | this.speechEvents = hark(this.props.localMedia, options);
60 | this.speechEvents.on('volume_change', (vol, threshold) => {
61 | this.setState({volume: 2 * (vol + 75)});
62 | });
63 | }
64 | }
65 |
66 | componentWillUnmount() {
67 | if (this.speechEvents !== null) {
68 | this.speechEvents.stop();
69 | this.speechEvents = null;
70 | }
71 | }
72 |
73 | render() {
74 | let color = 'primary';
75 | if (this.state.volume > 20) {
76 | color = 'secondary';
77 | }
78 | return (
79 |
80 | );
81 | }
82 | }
83 |
84 | VolumeBar.propTypes = {
85 | localMedia: PropTypes.object.isRequired,
86 | classes : PropTypes.object.isRequired
87 | };
88 |
89 |
90 | module.exports = withStyles(styleSheet)(VolumeBar);
91 |
--------------------------------------------------------------------------------
/src/app/components/WaveSurferPlayer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const useState = React.useState;
5 | const useEffect = React.useEffect;
6 | const useRef = React.useRef;
7 | const useCallback = React.useCallback;
8 |
9 | const {
10 | CircularProgress,
11 | IconButton,
12 | Grid
13 | } = require('@material-ui/core');
14 | const {
15 | PlayArrowRounded: PlayIcon,
16 | StopRounded: StopIcon
17 | } = require('@material-ui/icons');
18 | const { makeStyles } = require('@material-ui/core/styles');
19 | const { default: WaveSurfer } = require('wavesurfer.js');
20 |
21 |
22 | const styleSheet = makeStyles((theme) => ({
23 | root: {
24 | backgroundColor: '#337ab7',
25 | borderColor: '#2e6da4',
26 | color: '#fff',
27 | '&:hover': {
28 | backgroundColor: '#286090',
29 | borderColor: '#204d74',
30 | boxShadow: 'none'
31 | },
32 | '&:focus': {
33 | borderColor: '#122b40',
34 | backgroundColor: '#204d74',
35 | outlineOffset: '-2px',
36 | boxShadow: 'inset 0px 3px 5px 0px rgba(0,0,0,.125)'
37 | }
38 | }
39 | }));
40 |
41 | const useWavesurfer = (containerRef, options) => {
42 | const [wavesurfer, setWavesurfer] = useState(null)
43 |
44 | // Initialize wavesurfer when the container mounts
45 | // or any of the props change
46 | useEffect(() => {
47 | if (!containerRef.current) return
48 |
49 | const ws = WaveSurfer.create({
50 | ...options,
51 | container: containerRef.current
52 | })
53 |
54 | setWavesurfer(ws)
55 |
56 | return () => {
57 | ws.destroy()
58 | }
59 | }, [options, containerRef])
60 |
61 | return wavesurfer
62 | }
63 |
64 | const formatTime = (seconds) => {
65 | const minutes = Math.floor(seconds / 60)
66 | const secondsRemainder = Math.round(seconds) % 60
67 | const paddedSeconds = `0${secondsRemainder}`.slice(-2)
68 | return `${minutes}:${paddedSeconds}`
69 | }
70 |
71 | const WaveSurferPlayer = (props) => {
72 | const classes = styleSheet(props);
73 | const containerRef = useRef()
74 | const [isPlaying, setIsPlaying] = useState(false)
75 | const [currentTime, setCurrentTime] = useState(0)
76 | const [ready, setReady] = useState(false)
77 | const wavesurfer = useWavesurfer(containerRef, props)
78 |
79 | const onPlayClick = useCallback(() => {
80 | wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play()
81 | }, [wavesurfer])
82 |
83 | // Initialize wavesurfer when the container mounts
84 | // or any of the props change
85 | useEffect(() => {
86 | if (!wavesurfer) return
87 |
88 | setCurrentTime(0)
89 | setIsPlaying(false)
90 | let totalDuration = 0;
91 |
92 | const subscriptions = [
93 | wavesurfer.on('play', () => setIsPlaying(true)),
94 | wavesurfer.on('ready', () => setReady(true)),
95 | wavesurfer.on('pause', () => setIsPlaying(false)),
96 | wavesurfer.on('decode', (duration) => { totalDuration = duration; setCurrentTime(duration) }),
97 | wavesurfer.on('timeupdate', (currentTime) => setCurrentTime(totalDuration - currentTime))
98 | ]
99 |
100 | return () => {
101 | subscriptions.forEach((unsub) => unsub())
102 | }
103 | }, [wavesurfer])
104 |
105 | return (
106 |
107 |
108 |
109 |
110 | {isPlaying ?
111 |
112 | :
113 |
114 | }
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | {formatTime(currentTime)}
124 |
125 |
126 |
127 |
128 | {!ready &&
}
129 |
130 | )
131 | }
132 |
133 | module.exports = WaveSurferPlayer;
134 |
--------------------------------------------------------------------------------
/src/app/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const defaultDomain = 'sylk.link';
4 |
5 | const configOptions = {
6 | defaultDomain : defaultDomain,
7 | enrollmentDomain : defaultDomain,
8 | nonSipDomains : [], // Each domain configured here will be used for alternate authentication methods
9 | publicUrl : 'https://webrtc.sipthor.net',
10 | enrollmentUrl : 'https://blink.sipthor.net/enrollment-sylk-mobile.phtml',
11 | useServerCallHistory : true,
12 | serverCallHistoryUrl : 'https://blink.sipthor.net/settings-webrtc.phtml',
13 | defaultConferenceDomain : 'videoconference.sip2sip.info',
14 | defaultGuestDomain : `guest.${defaultDomain}`,
15 | wsServer : 'wss://webrtc-gateway.sipthor.net:9999/webrtcgateway/ws',
16 | fileSharingUrl : 'https://webrtc-gateway.sipthor.net:9999/webrtcgateway/filesharing',
17 | fileTransferUrl : 'https://webrtc-gateway.sipthor.net:9999/webrtcgateway/filetransfer',
18 | iceServers : [{urls: 'stun:stun.sipthor.net:3478'}],
19 | muteGuestAudioOnJoin : false,
20 | guestUserPermissions : {
21 | allowMuteAllParticipants : false,
22 | allowToggleHandsParticipants : false
23 | },
24 | showGuestCompleteScreen : true,
25 | downloadUrl : 'https://sylkserver.com'
26 | };
27 |
28 |
29 | module.exports = configOptions;
30 |
--------------------------------------------------------------------------------
/src/app/history.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const storage = require('./storage');
4 |
5 |
6 | function add(uri) {
7 | return load().then((history) => {
8 | if (history) {
9 | const idx = history.indexOf(uri);
10 | if (idx !== -1) {
11 | history.splice(idx, 1);
12 | }
13 | history.unshift(uri);
14 | // keep just the last 50
15 | history = history.slice(0, 50);
16 | } else {
17 | history = [uri];
18 | }
19 | storage.set('history', history);
20 | return history;
21 | });
22 | }
23 |
24 | function load() {
25 | return storage.get('history');
26 | }
27 |
28 | function clear() {
29 | return storage.remove('history');
30 | }
31 |
32 | exports.add = add;
33 | exports.load = load;
34 | exports.clear = clear;
35 |
--------------------------------------------------------------------------------
/src/app/hooks/index.js:
--------------------------------------------------------------------------------
1 | const { usePrevious } = require('./usePrevious');
2 | const { useResize } = require('./useResize');
3 | const { useHasChanged } = require('./useHasChanged');
4 |
5 | export { usePrevious, useResize, useHasChanged };
6 |
--------------------------------------------------------------------------------
/src/app/hooks/useHasChanged.js:
--------------------------------------------------------------------------------
1 | const { usePrevious } = require('./');
2 |
3 | const useHasChanged = (value) => {
4 | const previousValue = usePrevious(value);
5 | return JSON.stringify(previousValue) !== JSON.stringify(value);
6 | }
7 |
8 | export { useHasChanged };
9 |
10 |
--------------------------------------------------------------------------------
/src/app/hooks/usePrevious.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const React = require('react');
4 | const useEffect = React.useEffect;
5 | const useRef = React.useRef;
6 |
7 | const usePrevious = (value) => {
8 | const ref = useRef([]);
9 | useEffect(() => {
10 | ref.current = value;
11 | }, [value]);
12 |
13 | return ref.current;
14 | }
15 |
16 | export { usePrevious };
17 |
--------------------------------------------------------------------------------
/src/app/hooks/useResize.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const useEffect = React.useEffect;
3 | const useState = React.useState;
4 |
5 | const useResize = () => {
6 | const [myRef, setRef] = useState(null);
7 | const [width, setWidth] = useState(0);
8 | const [height, setHeight] = useState(0);
9 |
10 | useEffect(() => {
11 | const handleResize = () => {
12 | if (!ignore) {
13 | setWidth(myRef.offsetWidth)
14 | setHeight(myRef.offsetHeight)
15 | }
16 | }
17 | let ignore = false;
18 |
19 | if (myRef) {
20 | window.addEventListener('resize', handleResize)
21 | handleResize()
22 | }
23 |
24 | return () => {
25 | ignore = true;
26 | window.removeEventListener('resize', handleResize)
27 | }
28 | }, [myRef])
29 |
30 | return [setRef, width, height]
31 | }
32 |
33 | export { useResize };
34 |
--------------------------------------------------------------------------------
/src/app/mixins/FullScreen.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const screenfull = require('screenfull');
4 |
5 |
6 | const FullscreenMixin = {
7 | isFullScreen: function() {
8 | return screenfull.isFullscreen;
9 | },
10 |
11 | isFullscreenSupported: function() {
12 | return screenfull.isEnabled;
13 | },
14 |
15 | requestFullscreen: function(elem) {
16 | if (screenfull.isEnabled) {
17 | screenfull.request(elem);
18 | }
19 | },
20 |
21 | exitFullscreen: function() {
22 | if (screenfull.isEnabled) {
23 | screenfull.exit();
24 | }
25 | },
26 |
27 | toggleFullscreen: function(elem) {
28 | if (screenfull.isEnabled) {
29 | screenfull.toggle(elem);
30 | }
31 | }
32 | };
33 |
34 | module.exports = FullscreenMixin;
35 |
--------------------------------------------------------------------------------
/src/app/utils/Queue.js:
--------------------------------------------------------------------------------
1 | class Queue {
2 | static queue = [];
3 | static pendingPromise = false;
4 |
5 | static enqueue(promise) {
6 | return new Promise((resolve, reject) => {
7 | this.queue.push({
8 | promise,
9 | resolve,
10 | reject
11 | });
12 | this.dequeue();
13 | });
14 | }
15 |
16 | static dequeue() {
17 | if (this.workingOnPromise) {
18 | return false;
19 | }
20 | const item = this.queue.shift();
21 | if (!item) {
22 | return false;
23 | }
24 | try {
25 | this.workingOnPromise = true;
26 | item.promise()
27 | .then((value) => {
28 | this.workingOnPromise = false;
29 | item.resolve(value);
30 | this.dequeue();
31 | })
32 | .catch(err => {
33 | this.workingOnPromise = false;
34 | item.reject(err);
35 | this.dequeue();
36 | })
37 | } catch (err) {
38 | this.workingOnPromise = false;
39 | item.reject(err);
40 | this.dequeue();
41 | }
42 | return true;
43 | }
44 | }
45 |
46 | export { Queue };
47 |
--------------------------------------------------------------------------------
/src/assets/images/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/32.png
--------------------------------------------------------------------------------
/src/assets/images/blink-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/blink-48.png
--------------------------------------------------------------------------------
/src/assets/images/blink-grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/blink-grey.png
--------------------------------------------------------------------------------
/src/assets/images/blink-grey@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/blink-grey@2x.png
--------------------------------------------------------------------------------
/src/assets/images/blink-white-big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/blink-white-big.png
--------------------------------------------------------------------------------
/src/assets/images/blink-white-big@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/blink-white-big@2x.png
--------------------------------------------------------------------------------
/src/assets/images/blink-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/blink-white.png
--------------------------------------------------------------------------------
/src/assets/images/blink-white@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/blink-white@2x.png
--------------------------------------------------------------------------------
/src/assets/images/blink.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/blink.ico
--------------------------------------------------------------------------------
/src/assets/images/dark_linen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/dark_linen.png
--------------------------------------------------------------------------------
/src/assets/images/dark_linen@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/dark_linen@2x.png
--------------------------------------------------------------------------------
/src/assets/images/noise1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/noise1.png
--------------------------------------------------------------------------------
/src/assets/images/noise_dark2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/noise_dark2.png
--------------------------------------------------------------------------------
/src/assets/images/transparent-1px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/transparent-1px.png
--------------------------------------------------------------------------------
/src/assets/images/video-camera-slash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/images/video-camera-slash.png
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/0.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/0.wav
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/1.wav
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/2.wav
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/3.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/3.wav
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/4.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/4.wav
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/5.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/5.wav
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/6.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/6.wav
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/7.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/7.wav
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/8.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/8.wav
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/9.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/9.wav
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/hash.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/hash.wav
--------------------------------------------------------------------------------
/src/assets/sounds/dtmf/star.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/dtmf/star.wav
--------------------------------------------------------------------------------
/src/assets/sounds/hangup_tone.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/hangup_tone.wav
--------------------------------------------------------------------------------
/src/assets/sounds/inbound_ringtone.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/inbound_ringtone.wav
--------------------------------------------------------------------------------
/src/assets/sounds/outbound_ringtone.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/outbound_ringtone.wav
--------------------------------------------------------------------------------
/src/assets/sounds/participant_joined.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/participant_joined.wav
--------------------------------------------------------------------------------
/src/assets/sounds/participant_left.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AGProjects/sylk-webrtc/9591dd7f07f8b2ad27da231d7148c1b473bb5254/src/assets/sounds/participant_left.wav
--------------------------------------------------------------------------------
/src/assets/styles/blink.scss:
--------------------------------------------------------------------------------
1 | // Blink (http://icanblink.com)
2 |
3 | // Variables
4 | @import 'blink/variables';
5 |
6 | // Sass functions
7 | @import 'blink/functions';
8 |
9 | // Mixins
10 | @import 'blink/mixins';
11 |
12 | // Base
13 | @import 'blink/base';
14 |
15 | // Utils
16 | @import 'blink/utils';
17 |
18 | // Calls
19 | @import 'blink/call';
20 |
21 | // Video
22 | @import 'blink/video';
23 |
24 | // Notifications
25 | @import 'blink/notifications';
26 |
27 | // Forms
28 | @import 'blink/forms';
29 |
30 | // Buttons
31 | @import 'blink/buttons';
32 |
33 | // Modals
34 | @import 'blink/modals';
35 |
36 | // Popovers
37 | @import 'blink/popovers';
38 |
39 | // Animations
40 | @import 'blink/animations';
41 |
42 | // Conference
43 | @import 'blink/conference';
44 |
45 | // Conference Drawer
46 | @import 'blink/conferenceDrawer';
47 |
48 | // Components:
49 |
50 | // Loading Screen
51 | @import 'blink/LoadingScreen';
52 |
53 | // Navbar
54 | @import 'blink/NavigationBar';
55 |
56 | // URIInput
57 | @import 'blink/URIInput';
58 |
59 | // DTMFModal
60 | @import 'blink/DTMFModal';
61 |
62 | // Preview
63 | @import 'blink/Preview';
64 |
65 | // History Tile Box
66 | @import 'blink/HistoryTileBox';
67 |
68 | // Toolbar audio player
69 | @import 'blink/_ToolbarAudioPlayer';
70 |
71 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_DTMFModal.scss:
--------------------------------------------------------------------------------
1 |
2 | // Custom default button for DTMF keypad
3 | .btn-dtmf {
4 | &,
5 | &:hover,
6 | &:focus {
7 | color: $gray;
8 | text-shadow: none; // Prevent inheritence from `body`
9 | background-color: $white;
10 | border: 1px solid $gray;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_HistoryTileBox.scss:
--------------------------------------------------------------------------------
1 | .history-tile-box {
2 | display: flex;
3 | flex: 1 0 auto;
4 | align-items: self-start;
5 | width: 100%;
6 | max-width: 1200px;
7 | margin: auto;
8 |
9 | // Show hide history cards
10 | .card-hidden {
11 | height: 100%;
12 | visibility: hidden;
13 | }
14 |
15 | .card-visible {
16 | height: 100%;
17 | visibility: visible;
18 | animation-delay: .3s;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_LoadingScreen.scss:
--------------------------------------------------------------------------------
1 | .loading {
2 | display: table;
3 | width: 100%;
4 | height: 100%; // For at least Firefox
5 | min-height: 100%;
6 | }
7 |
8 | .loading-inner {
9 | display: table-cell;
10 | vertical-align: middle;
11 | }
12 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_NavigationBar.scss:
--------------------------------------------------------------------------------
1 | // Values
2 | // 31 -> inner buttonsize + border
3 | // 38 -> default padding is 6 on a 50px 50-12
4 | // 20 -> default line-height
5 |
6 | .navbar-header {
7 | float: left !important;
8 | }
9 |
10 | .navbar-blink-logo {
11 | @include background-image-retina($navbar-logo-image, $navbar-height, $navbar-height);
12 | width: $navbar-height;
13 | height: $navbar-height;
14 | margin: 0 auto;
15 | margin-left: 15px;
16 | }
17 |
18 | .navbar-btn-toolbar {
19 | margin-top: ($navbar-height - 31 - zero($navbar-height - 38)) * 0.5; // 40 -> 7/2, 50 -> 7/2
20 |
21 | button.active {
22 | padding-top: 6 + ($navbar-height - 31 - zero($navbar-height - 38)) * 0.5; // 40 -> 7/2, 50 -> 7/2
23 | padding-bottom: 4 + ($navbar-height - 31 - zero($navbar-height - 38)) * 0.5; // 40 -> 7/2, 50 -> 7/2
24 | margin-top: -($navbar-height - 31 - zero($navbar-height - 38)) * 0.5; // 40 -> 7/2, 50 -> 7/2
25 | color: #fff;
26 | background-color: rgb(8, 8, 8);
27 | }
28 | }
29 |
30 | .navbar {
31 | z-index: 1201;
32 | min-height: $navbar-height;
33 |
34 | button {
35 | padding: zero(($navbar-height - 38) * 0.5) 12px;
36 |
37 | &.btn-fw {
38 | width: 58px;
39 | margin-right: -5px;
40 | }
41 | }
42 | }
43 |
44 | .navbar-brand {
45 | height: $navbar-height;
46 | padding: ($navbar-height - 20) * 0.5 15px;
47 | padding-left: 0;
48 | margin-left: 0 !important;
49 | }
50 |
51 | .navbar-text {
52 | margin: ($navbar-height - 20) * 0.5 15px;
53 | }
54 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_Preview.scss:
--------------------------------------------------------------------------------
1 | .video-icon {
2 | &.drawer-visible {
3 | width: calc(100% - 350px) !important;
4 | margin-right: 350px;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_URIInput.scss:
--------------------------------------------------------------------------------
1 | div {
2 | &.uri-input {
3 | .algolia-autocomplete {
4 | width: 100%;
5 |
6 | .aa-hint {
7 | color: $lighter-gray;
8 | }
9 |
10 | .aa-input,
11 | .aa-hint {
12 | width: 100%;
13 | }
14 |
15 | .aa-dropdown-menu {
16 | width: 100%;
17 | background-color: $white;
18 | border: 1px solid $lighter-gray;
19 | border-top: 0;
20 |
21 | .aa-suggestion {
22 | padding: 5px 4px;
23 | color: $gray;
24 | text-align: left;
25 | cursor: pointer;
26 |
27 | &.aa-cursor {
28 | color: darken($gray, 5%);
29 | background-color: $bootstrap-link-hover-bg;
30 | }
31 | }
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_animations.scss:
--------------------------------------------------------------------------------
1 | // Sound animation
2 |
3 | .animate-sound1 {
4 | animation: fade-custom 3s ease-in-out infinite;
5 | }
6 |
7 | @keyframes fade-custom {
8 | from {opacity: 0;}
9 | to {opacity: 1;}
10 | }
11 |
12 |
13 | // BELL
14 | @keyframes ring {
15 | 0% {
16 | transform: rotate(-15deg);
17 | }
18 |
19 | 2% {
20 | transform: rotate(15deg);
21 | }
22 |
23 | 4% {
24 | transform: rotate(-18deg);
25 | }
26 |
27 | 6% {
28 | transform: rotate(18deg);
29 | }
30 |
31 | 8% {
32 | transform: rotate(-22deg);
33 | }
34 |
35 | 10% {
36 | transform: rotate(22deg);
37 | }
38 |
39 | 12% {
40 | transform: rotate(-18deg);
41 | }
42 |
43 | 14% {
44 | transform: rotate(18deg);
45 | }
46 |
47 | 16% {
48 | transform: rotate(-12deg);
49 | }
50 |
51 | 18% {
52 | transform: rotate(12deg);
53 | }
54 |
55 | 20% {
56 | transform: rotate(0deg);
57 | }
58 | }
59 |
60 | .faa-ring {
61 | &.animated {
62 | animation: ring 2s ease infinite;
63 | transform-origin-x: 50%;
64 | transform-origin-y: 0;
65 | transform-origin-z: initial;
66 |
67 | &.faa-fast {
68 | animation: ring 1s ease infinite;
69 | }
70 |
71 | &.faa-slow {
72 | animation: ring 3s ease infinite;
73 | }
74 | }
75 | }
76 |
77 | // Incoming Modal animation
78 | .incoming-modal-enter {
79 | animation: fadeIn ease .3s;
80 | }
81 |
82 | .incoming-modal-exit {
83 | &.incoming-modal-exit-active {
84 | animation: fadeOut ease .3s;
85 | }
86 | }
87 |
88 | .premedia-display-enter-active {
89 | animation: fadeIn ease .3s;
90 | }
91 |
92 | .premedia-display-exit {
93 | &.premedia-display-exit-active {
94 | animation: fadeOut ease .5s;
95 | }
96 | }
97 |
98 | .premedia-display-exit-done {
99 | visibility: hidden;
100 | opacity: 0;
101 | }
102 |
103 | /* starting ENTER animation */
104 | .message-enter {
105 | opacity: 0;
106 | }
107 |
108 | /* ending ENTER animation */
109 | .message-enter-active {
110 | opacity: 1;
111 | transition: all 1000ms ease-in-out;
112 | }
113 |
114 | /* starting EXIT animation */
115 | .message-exit {
116 | opacity: 1;
117 | }
118 |
119 | /* ending EXIT animation */
120 | .messageout-exit-active {
121 | opacity: 0;
122 | transition: opacity 1000ms ease-in-out;
123 | }
124 |
125 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_base.scss:
--------------------------------------------------------------------------------
1 | // Base structure
2 |
3 | html,
4 | body {
5 | height: 100%;
6 | overflow: hidden;
7 | }
8 |
9 | body {
10 | @include background-image-retina($base-background-image, 512px, 512px);
11 | color: $base-foreground-color;
12 | text-align: center;
13 | background-color: $base-background-color;
14 | }
15 |
16 | h1,
17 | h2,
18 | h3,
19 | h4,
20 | h5,
21 | h6,
22 | .h1,
23 | .h2,
24 | .h3,
25 | .h4,
26 | .h5 {
27 | font-weight: 300;
28 | }
29 |
30 | // This will get the negative margin when >768px
31 | %site-wrapper {
32 | display: table;
33 | width: 100%;
34 | height: 100%; // For at least Firefox
35 | min-height: 100%;
36 | }
37 |
38 | .site-wrapper {
39 | @extend %site-wrapper;
40 | }
41 |
42 | // Extra markup and styles for table-esque vertical and horizontal centering
43 | .site-wrapper-shadow {
44 | @extend %site-wrapper;
45 | box-shadow: inset 0 0 100px $black-transparent;
46 | }
47 |
48 | .site-wrapper-inner {
49 | display: table-cell;
50 | vertical-align: middle;
51 | }
52 |
53 | .cover-container {
54 | margin-right: auto;
55 | margin-left: auto;
56 | }
57 |
58 |
59 | // Padding for spacing
60 | .inner {
61 | padding: 30px;
62 | }
63 |
64 | .inner-small {
65 | padding: 10px;
66 | }
67 |
68 | .blink-logo {
69 | @include background-image-retina($base-logo-image, 125px, 125px);
70 | width: 125px;
71 | height: 125px;
72 | margin: 0 auto;
73 | }
74 |
75 | // Stckiy layer to top
76 | .sticky-wrapper {
77 | position: sticky;
78 | top: -1px;
79 | z-index: 1;
80 | margin-top: calc(50vh - 159px);
81 | margin-right: -20px;
82 | margin-left: -20px;
83 | }
84 |
85 | .sticky {
86 | //sass-lint:disable-block no-color-literals, property-sort-order
87 | /* stylelint-disable order/properties-order,declaration-block-no-shorthand-property-overrides */
88 | background-color: $black-less-transparent;
89 | background: linear-gradient(180deg, $black 0%, rgba($black, .95) 15%, rgba($black, .55) 80%, rgba($black, 0) 100%);
90 | /* stylelint-enable */
91 | }
92 |
93 |
94 | // Cover
95 |
96 | .cover {
97 | padding: 0 20px;
98 |
99 | .btn-lg {
100 | padding: 10px 20px;
101 | font-weight: bold;
102 | }
103 | }
104 |
105 | // Scroll main section
106 |
107 | .scroll {
108 | display: flex;
109 | flex-direction: column;
110 | height: calc(100vh - 50px);
111 | margin-top: 50px;
112 | overflow-x: hidden;
113 | overflow-y: auto;
114 |
115 | .footer {
116 | position: static;
117 | padding-top: 10px;
118 | }
119 | }
120 |
121 | // Footer
122 |
123 | .footer {
124 | position: fixed;
125 | bottom: 0;
126 | font-size: 11px;
127 | color: $white-transparent;
128 | text-shadow: 0 1px 3px $black-transparent;
129 | }
130 |
131 | // Handle the widths
132 | .footer,
133 | .cover-container,
134 | .half-width {
135 | width: 100%; // Must be percentage or pixels for horizontal alignment
136 | }
137 |
138 |
139 | @media (min-width: 992px) {
140 | .half-width {
141 | width: 500px;
142 | margin: auto;
143 | }
144 | }
145 |
146 | // Helper to include shadow if footer is not displayed from main
147 | .extra-shadow {
148 | position: fixed;
149 | bottom: -100px;
150 | z-index: 10;
151 | width: 100vw;
152 | height: 100px;
153 | margin: 0 -20px;
154 | box-shadow: 0 -5px 100px $black-transparent;
155 | }
156 |
157 | .chat-image {
158 | width: 300px;
159 | height: 300px;
160 | @include background-image-retina($video-big-poster-image, 300px, 300px);
161 | filter: brightness(75%);
162 | opacity: .8;
163 | }
164 |
165 | .chat {
166 | .conference-drawer {
167 | .editor-wrapper {
168 | border: 0;
169 | box-shadow: none;
170 | }
171 | .top-editor-wrapper {
172 | padding-top: 10px;
173 | border-top: 1px solid rgba(0, 0, 0, 0.12);
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_buttons.scss:
--------------------------------------------------------------------------------
1 | // Custom default button
2 | .btn-default {
3 | &,
4 | &:hover,
5 | &:focus {
6 | color: $default-button-foreground-color;
7 | text-shadow: none; // Prevent inheritence from `body`
8 | background-color: $default-button-background-color;
9 | border: 1px solid $default-button-background-color;
10 | }
11 | }
12 |
13 | // Round Button
14 |
15 | %btn-round {
16 | margin: 4px;
17 | border-radius: 50%;
18 | opacity: .9;
19 | }
20 |
21 | .btn-round {
22 | @extend %btn-round;
23 | width: 45px;
24 | height: 45px;
25 | font-size: 20px;
26 | }
27 |
28 | .btn-round-big {
29 | @extend %btn-round;
30 | width: 55px;
31 | height: 55px;
32 | font-size: 22px;
33 | }
34 |
35 | .btn-round-xxl {
36 | @extend %btn-round;
37 | width: 65px;
38 | height: 65px;
39 | font-size: 24px;
40 | }
41 |
42 | .btn-round-xs {
43 | @extend %btn-round;
44 | width: 20px;
45 | height: 20px;
46 | padding: 0;
47 | font-size: 10px;
48 | }
49 |
50 | .btn-link,
51 | .btn-round,
52 | .btn-round-big,
53 | .btn-round-xxl,
54 | .btn-round-xs {
55 | &:focus {
56 | &,
57 | &:active {
58 | outline: 0;
59 | }
60 | }
61 | }
62 |
63 | .overlap {
64 | position: absolute;
65 | bottom: 0;
66 | margin-left: -20px;
67 | background-color: $light-gray !important;
68 | border: $lighter-gray;
69 | box-shadow: 0 1px 4px rgba(0,0,0,.25);
70 | }
71 |
72 | .overlap-top {
73 | position: absolute;
74 | top: 0;
75 | margin-left: -20px;
76 | }
77 |
78 | .btn-container {
79 | position: relative;
80 | display: inline-block;
81 | }
82 |
83 | .btn-hand {
84 | padding-top: 0;
85 | padding-bottom: 0;
86 | color: $white;
87 |
88 | &:hover {
89 | color: $bootstrap-primary;
90 | }
91 |
92 | i {
93 | font-size: 21px;
94 | }
95 | }
96 |
97 | .media-right {
98 | .btn-hand {
99 | padding-top: 0;
100 | padding-bottom: 0;
101 | color: $gray;
102 |
103 | &:hover {
104 | color: $bootstrap-primary;
105 | }
106 |
107 | i {
108 | font-size: inherit;
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_call.scss:
--------------------------------------------------------------------------------
1 | .call-buttons {
2 | position: absolute;
3 | right: 0;
4 | bottom: 0 !important;
5 | left: 0;
6 | z-index: 1;
7 | padding-bottom: 35px;
8 | margin: auto;
9 | overflow: hidden;
10 | }
11 |
12 | .top-overlay {
13 | position: absolute;
14 | top: 0;
15 | right: 0;
16 | left: 0;
17 | z-index: 2;
18 |
19 | &.on-top {
20 | z-index: 1500;
21 | }
22 | }
23 |
24 | .drawer-visible {
25 | position: absolute;
26 | top: 0;
27 | right:0;
28 | bottom: 0;
29 | left: 0;
30 | display: flex;
31 | width: calc(100% - 350px) !important;
32 | margin-right: 350px;
33 | }
34 |
35 | .drawer-half-visible {
36 | @extend .drawer-visible;
37 | width: 50% !important;
38 | }
39 |
40 | .drawer-wide-visible {
41 | @extend .drawer-visible;
42 | width: calc(100% - 450px) !important;
43 | margin-right: 450px;
44 | }
45 |
46 | .call-header {
47 | padding: 4px 0;
48 | color: $white;
49 | background-color: $darker-gray-transparent;
50 |
51 | p {
52 | margin-bottom: 2px !important;
53 | overflow: hidden;
54 | font-size: 17px !important;
55 | line-height: 1.2 !important;
56 | text-overflow: ellipsis;
57 | }
58 |
59 | p + p {
60 | margin-bottom: 0 !important;
61 | }
62 |
63 | &.solid-background {
64 | background-color: $darker-gray;
65 | }
66 |
67 | .btn-link {
68 | padding-top: 9.5px;
69 | padding-bottom: 7.5px;
70 | margin-top: -3.5px;
71 | color: $lighter-gray;
72 |
73 | &:hover {
74 | color: $white;
75 | }
76 |
77 | &.active {
78 | color: #fff;
79 | background-color: rgb(8, 8, 8);
80 | }
81 |
82 | }
83 |
84 | .blink {
85 | animation: blink 1s infinite;
86 | animation-direction: alternate;
87 | }
88 |
89 | @keyframes blink {
90 | 0% { color: #5cb85c; opacity: 1; }
91 | 50% { color: #5cb85c;transform: scale(1.1); }
92 | 100% { color: #5cb85c;transform: scale(0.9); }
93 | }
94 |
95 | .call-top-left-buttons {
96 | position: absolute;
97 | top: 0;
98 | left: 0;
99 | margin-top: 3.5px;
100 |
101 | }
102 |
103 | .call-top-buttons {
104 | position: absolute;
105 | top: 0;
106 | right: 0;
107 | margin-top: 3.5px;
108 | }
109 |
110 |
111 | }
112 |
113 | .call-user-icon {
114 | padding-bottom: 100px;
115 | margin: auto;
116 | }
117 |
118 | // Buttons Animation
119 |
120 | .videobuttons-enter {
121 | animation: fadeInUp linear .3s;
122 | }
123 |
124 | .videobuttons-exit {
125 | &.videobuttons-exit-active {
126 | animation: fadeOutDown linear .3s;
127 | }
128 | }
129 |
130 | // Video header animation
131 | .videoheader-enter {
132 | animation: fadeInDown linear .3s;
133 | }
134 |
135 | .videoheader-exit {
136 | &.videoheader-exit-active {
137 | animation: fadeOutUp linear .3s;
138 | }
139 | }
140 |
141 | // Watermark animation
142 | .watermark-enter {
143 | animation: fadeIn linear .6s;
144 | }
145 |
146 | .watermark-exit {
147 | &.watermark-exit-active {
148 | animation: fadeOut linear .3s;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_forms.scss:
--------------------------------------------------------------------------------
1 | // General
2 |
3 | // Forms
4 |
5 | .form-dial,
6 | .form-guest,
7 | .form-signin {
8 | max-width: 330px;
9 | padding: 15px;
10 | margin: 0 auto;
11 | }
12 |
13 | .form-dial {
14 | max-width: 360px;
15 | }
16 |
17 | .form-signin {
18 | .checkbox,
19 | .form-signin-heading {
20 | margin-bottom: 10px;
21 | }
22 |
23 | .checkbox {
24 | font-weight: normal;
25 | }
26 |
27 | input {
28 | &[type='password'] {
29 | border-top-left-radius: 0;
30 | border-top-right-radius: 0;
31 | }
32 | }
33 |
34 | .input-group {
35 | &:first-of-type {
36 | input {
37 | margin-bottom: -1px;
38 | border-bottom-right-radius: 0;
39 | border-bottom-left-radius: 0;
40 |
41 | // Needed to display blue border over next input group
42 | &:focus {
43 | z-index: 3;
44 | }
45 | }
46 |
47 | .input-group-addon {
48 | &:first-child {
49 | padding: 10px;
50 | padding-top: 11px;
51 | margin-bottom: -1px;
52 | font-size: 16px;
53 | border: 0;
54 | border-bottom-right-radius: 0;
55 | border-bottom-left-radius: 0;
56 | }
57 | }
58 | }
59 |
60 | ~.input-group {
61 | .input-group-addon {
62 | &:first-child {
63 | padding: 10px;
64 | padding-top: 12px;
65 | padding-bottom: 11px;
66 | font-size: 16px;
67 | border: 0;
68 | border-top: 1px solid $light-gray;
69 | border-top-left-radius: 0;
70 | border-top-right-radius: 0;
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
77 | .form-guest,
78 | .form-signin-electron {
79 | .input-group {
80 | ~.input-group {
81 | margin-bottom: 20px;
82 | }
83 | }
84 | }
85 |
86 | .form-guest,
87 | .form-signin {
88 | .form-control {
89 | position: relative;
90 | box-sizing: border-box;
91 | height: auto;
92 | padding: 10px;
93 | font-size: 16px;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_functions.scss:
--------------------------------------------------------------------------------
1 | @function zero($number) {
2 | $value: 0;
3 |
4 | @if $number > 0 {
5 | $value: $number;
6 | }
7 |
8 | @return $value;
9 | }
10 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin background-image-retina($file, $width: auto, $height: auto, $type: 'png') {
2 | background-image: url($file + '.' + $type);
3 | background-size: $width $height;
4 | @media only screen and (min-resolution: 1.5dppx) {
5 | & {
6 | background-image: url($file + '@2x.' + $type);
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_modals.scss:
--------------------------------------------------------------------------------
1 |
2 | .modal-content {
3 | a {
4 | &,
5 | &:focus,
6 | &:hover {
7 | color: $bootstrap-primary;
8 | }
9 |
10 | &.btn-primary {
11 | color: $white;
12 | }
13 | }
14 | }
15 |
16 | .modal-body {
17 | .lead {
18 | font-size: 16px;
19 | }
20 | }
21 |
22 | .modal,
23 | .modal-backdrop {
24 | z-index: 9999 !important;
25 | }
26 |
27 | // Modal colors, text should not be white
28 |
29 | .modal-body,
30 | .modal-header,
31 | .modal-footer {
32 | color: $gray;
33 | }
34 |
35 | .modal-danger > .modal-content > .modal-header {
36 | padding: 10px;
37 | color: $bootstrap-danger !important;
38 | background-color: $bootstrap-danger-bg;
39 | border-color: $bootstrap-danger-border;
40 | border-radius: 6px 6px 0 0;
41 | }
42 |
43 | .modal-danger > .modal-content {
44 | border-color: $bootstrap-danger-border;
45 | }
46 |
47 | .bd {
48 | background: linear-gradient(transparent, rgba(255,255,255,.9)) !important;
49 | }
50 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_notifications.scss:
--------------------------------------------------------------------------------
1 | .notifications-tr {
2 | right: 40px !important;
3 | z-index: 1500 !important;
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_popovers.scss:
--------------------------------------------------------------------------------
1 | .popover {
2 | color: $gray;
3 |
4 | &.on-top {
5 | z-index: 2000;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_utils.scss:
--------------------------------------------------------------------------------
1 | .semi-transparent {
2 | opacity: .7;
3 | }
4 |
5 | // Rotate 135 degrees
6 |
7 | .rotate-135 {
8 | transform: rotate(135deg);
9 | }
10 |
11 | // Bigger fa-4
12 |
13 | .fa-4 {
14 | font-size: 7em;
15 | }
16 |
17 | .fa-5 {
18 | font-size: 8em;
19 | }
20 | // First letter is a capital
21 |
22 | .capitalize {
23 | text-transform: capitalize;
24 | }
25 |
26 | // Shift icons so they overlap
27 |
28 | // TODO: rename
29 | .move-icon {
30 | z-index: 0;
31 | margin-left: -21px;
32 | }
33 |
34 | .move-icon2 {
35 | z-index: 1;
36 | margin-left: 28px;
37 | }
38 |
39 | .blue-bar {
40 | background-color: $bootstrap-primary !important;
41 | }
42 |
43 | .rotate-minus-45 {
44 | transform: rotate(-45deg);
45 | }
46 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_variables.scss:
--------------------------------------------------------------------------------
1 | $black: #000;
2 | $gray: #333;
3 | $darker-gray: #222;
4 | $lighter-gray: #999;
5 | $gray888: #888;
6 | $light-gray: #ccc;
7 | $white: #fff;
8 |
9 | $black-transparent: rgba($black, .5);
10 | $black-less-transparent: rgba($black, .7);
11 | $darker-gray-transparent: rgba($darker-gray, .7);
12 | $white-transparent: rgba($white, .5);
13 | $white-less-transparent: rgba($white, .8);
14 |
15 | $bootstrap-danger: #a94442;
16 | $bootstrap-danger-bg: #f2dede;
17 | $bootstrap-danger-border: darken(adjust-hue($bootstrap-danger-bg, -10), 5%);
18 | $bootstrap-base: #428bca;
19 | $bootstrap-primary: darken($bootstrap-base, 6.5%); // #337ab7
20 | $bootstrap-link-hover-bg: #f5f5f5;
21 | $bootstrap-text-info: #31708f;
22 |
23 | $base-foreground-color: $white;
24 | $base-background-color: $gray;
25 |
26 | $default-button-foreground-color: $gray;
27 | $default-button-background-color: $white;
28 |
29 | $base-background-image: '../images/dark_linen';
30 |
31 | // Needs to be 125 x 125px
32 | $base-logo-image: '../images/blink-white';
33 |
34 | $navbar-logo-image: '../images/blink-white';
35 | $navbar-height: 50px;
36 |
37 | $video-poster-image: '../images/blink-white';
38 | $video-big-poster-image: '../images/blink-white-big';
39 | $video-watermark-image: '../images/aglogo-white.svg';
40 |
41 | $video-thumbnail-width: 150px;
42 | $video-thumbnail-height: 100px;
43 |
--------------------------------------------------------------------------------
/src/assets/styles/blink/_video.scss:
--------------------------------------------------------------------------------
1 | .video-container {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | width: 100% !important;
6 | height: 100% !important;
7 | overflow: hidden;
8 |
9 | &.drawer-visible {
10 | width: calc(100% - 350px) !important;
11 | margin-right: 350px;
12 | }
13 |
14 | video {
15 | object-fit: cover;
16 |
17 | &.mirror {
18 | transform: scaleX(-1) !important; // mirror
19 | }
20 |
21 | &.large {
22 | z-index: 0;
23 | width: 100% !important;
24 | height: auto !important;
25 | min-height: 100%;
26 | max-height: 100%;
27 | }
28 |
29 | &.video-thumbnail {
30 | position: absolute;
31 | bottom: 10px;
32 | left: 30px;
33 | z-index: 3;
34 | width: $video-thumbnail-width !important;
35 | height: $video-thumbnail-height !important;
36 | border: 2px solid $white;
37 | border-radius: 10px;
38 | }
39 |
40 | &.poster {
41 | @include background-image-retina($video-poster-image, 75px, 75px);
42 | background-repeat: no-repeat;
43 | background-position: center;
44 | background-blend-mode: luminosity;
45 |
46 | &.large {
47 | @include background-image-retina($video-big-poster-image, 350px, 350px);
48 | background-color: $black-transparent;
49 | }
50 | }
51 |
52 | &.fit {
53 | background-color: $white-transparent;
54 | object-fit: contain !important;
55 | }
56 | }
57 |
58 | .watermark {
59 | position: absolute;
60 | top: 0;
61 | right: 5px;
62 | z-index: 2;
63 | width: 160px;
64 | height: 50px;
65 | background-image: url($video-watermark-image);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/incomingWindow.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Sylk
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
41 | This app works only with a JavaScript enabled browser.
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Sylk
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
33 |
34 |
35 |
36 |
37 |
38 |
41 |
42 | This app works only with a JavaScript enabled browser.
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------