├── .boring ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .parcelrc ├── .posthtmlrc_electron ├── .posthtmlrc_no_electron ├── .sass-lint.yml ├── .stylelintrc.json ├── LICENSE ├── Makefile ├── README.md ├── app ├── incomingWindow.js ├── index.js ├── osxTitleBar.js ├── package.json └── yarn.lock ├── build ├── afterSignHook.js ├── entitlements.mac.inherit.plist ├── entitlements.mac.plist ├── icon.icns ├── icon.ico └── icons │ └── 512.png ├── changelog.txt ├── configure ├── examples └── apache │ └── .htaccess ├── package.json ├── src ├── app │ ├── MaterialColors.js │ ├── MaterialUIAsBootstrap.js │ ├── SillyNames.js │ ├── app.js │ ├── cacheStorage.js │ ├── components │ │ ├── AboutModal.js │ │ ├── AudioCallBox.js │ │ ├── AudioPlayer.js │ │ ├── Call.js │ │ ├── CallByUriBox.js │ │ ├── CallCompleteBox.js │ │ ├── CallMeMaybeModal.js │ │ ├── CallOverlay.js │ │ ├── CallQuality.js │ │ ├── Chat.js │ │ ├── Chat │ │ │ ├── ContactList.js │ │ │ ├── FileTransferMessage.js │ │ │ ├── ImagePreviewModal.js │ │ │ ├── InfoPanel.js │ │ │ ├── Message.js │ │ │ ├── MessageList.js │ │ │ ├── OldMessage.js │ │ │ ├── ToolbarAudioPlayer.js │ │ │ ├── VoiceMessageRecorderModal.js │ │ │ └── VoiceMessageRecorderRenderer.js │ │ ├── Conference.js │ │ ├── ConferenceBox.js │ │ ├── ConferenceByUriBox.js │ │ ├── ConferenceCarousel.js │ │ ├── ConferenceChat.js │ │ ├── ConferenceChatEditor.js │ │ ├── ConferenceDrawer.js │ │ ├── ConferenceDrawerFiles.js │ │ ├── ConferenceDrawerLog.js │ │ ├── ConferenceDrawerMute.js │ │ ├── ConferenceDrawerParticipant.js │ │ ├── ConferenceDrawerParticipantList.js │ │ ├── ConferenceDrawerSpeakerSelection.js │ │ ├── ConferenceHeader.js │ │ ├── ConferenceMatrixParticipant.js │ │ ├── ConferenceMenu.js │ │ ├── ConferenceModal.js │ │ ├── ConferenceParticipant.js │ │ ├── ConferenceParticipantSelf.js │ │ ├── CustomContextMenu.js │ │ ├── DTMFModal.js │ │ ├── DividerWithText.js │ │ ├── DragAndDrop.js │ │ ├── EncryptionModal.js │ │ ├── EnrollmentModal.js │ │ ├── ErrorPanel.js │ │ ├── EscalateConferenceModal.js │ │ ├── FooterBox.js │ │ ├── HandIcon.js │ │ ├── HistoryCard.js │ │ ├── HistoryTileBox.js │ │ ├── ImportModal.js │ │ ├── IncomingCallModal.js │ │ ├── IncomingCallWindow.js │ │ ├── InviteParticipantsModal.js │ │ ├── ListWithStickyHeader.js │ │ ├── LoadingScreen.js │ │ ├── LocalMedia.js │ │ ├── Logo.js │ │ ├── LogoutModal.js │ │ ├── MessagesLoadingScreen.js │ │ ├── MuteAudioParticipantsModal.js │ │ ├── NavigationBar.js │ │ ├── NewDeviceModal.js │ │ ├── NotificationCenter.js │ │ ├── PreMedia.js │ │ ├── Preview.js │ │ ├── ReadyBox.js │ │ ├── RedialScreen.js │ │ ├── RegisterBox.js │ │ ├── RegisterForm.js │ │ ├── ScreenSharingModal.js │ │ ├── ShortcutsModal.js │ │ ├── Statistics.js │ │ ├── Statistics │ │ │ ├── AreaGradientChart.js │ │ │ ├── Charts.js │ │ │ └── LineChart.js │ │ ├── StatusBox.js │ │ ├── SwitchDevicesMenu.js │ │ ├── SwitchDevicesMenu │ │ │ ├── AudioMenuItem.js │ │ │ └── VideoMenuItem.js │ │ ├── SwitchDevicesModal.js │ │ ├── TabPanel.js │ │ ├── URIInput.js │ │ ├── UserIcon.js │ │ ├── VideoBox.js │ │ ├── VolumeBar.js │ │ └── WaveSurferPlayer.js │ ├── config.js │ ├── fileTransferUtils.js │ ├── history.js │ ├── hooks │ │ ├── index.js │ │ ├── useHasChanged.js │ │ ├── usePrevious.js │ │ └── useResize.js │ ├── keyStorage.js │ ├── messageStorage.js │ ├── mixins │ │ └── FullScreen.js │ ├── storage.js │ ├── utils.js │ └── utils │ │ └── Queue.js ├── assets │ ├── images │ │ ├── 32.png │ │ ├── aglogo-white.svg │ │ ├── blink-48.png │ │ ├── blink-grey.png │ │ ├── blink-grey@2x.png │ │ ├── blink-white-big.png │ │ ├── blink-white-big@2x.png │ │ ├── blink-white.png │ │ ├── blink-white@2x.png │ │ ├── blink.ico │ │ ├── dark_linen.png │ │ ├── dark_linen@2x.png │ │ ├── noise1.png │ │ ├── noise_dark2.png │ │ ├── transparent-1px.png │ │ └── video-camera-slash.png │ ├── sounds │ │ ├── dtmf │ │ │ ├── 0.wav │ │ │ ├── 1.wav │ │ │ ├── 2.wav │ │ │ ├── 3.wav │ │ │ ├── 4.wav │ │ │ ├── 5.wav │ │ │ ├── 6.wav │ │ │ ├── 7.wav │ │ │ ├── 8.wav │ │ │ ├── 9.wav │ │ │ ├── hash.wav │ │ │ └── star.wav │ │ ├── hangup_tone.wav │ │ ├── inbound_ringtone.wav │ │ ├── outbound_ringtone.wav │ │ ├── participant_joined.wav │ │ └── participant_left.wav │ └── styles │ │ ├── blink.scss │ │ └── blink │ │ ├── _DTMFModal.scss │ │ ├── _HistoryTileBox.scss │ │ ├── _LoadingScreen.scss │ │ ├── _NavigationBar.scss │ │ ├── _Preview.scss │ │ ├── _ToolbarAudioPlayer.scss │ │ ├── _URIInput.scss │ │ ├── _animations.scss │ │ ├── _base.scss │ │ ├── _buttons.scss │ │ ├── _call.scss │ │ ├── _conference.scss │ │ ├── _conferenceDrawer.scss │ │ ├── _forms.scss │ │ ├── _functions.scss │ │ ├── _mixins.scss │ │ ├── _modals.scss │ │ ├── _notifications.scss │ │ ├── _popovers.scss │ │ ├── _utils.scss │ │ ├── _variables.scss │ │ └── _video.scss ├── incomingWindow.html └── index.html ├── test └── tls │ └── test.pem └── yarn.lock /.boring: -------------------------------------------------------------------------------- 1 | 2 | (^|/)\.tmp($|/) 3 | (^|/)\.sass-cache($|/) 4 | (^|/)node_modules($|/) 5 | (^|/)src/js($|/) 6 | (^|/).DS_Store($|/) 7 | (^|/)dist($|/) 8 | (^|/)dist-electron($|/) 9 | (^|/)app/www($|/) 10 | (^|/).env($|/) 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "ecmaFeatures": { 6 | "jsx": true 7 | }, 8 | }, 9 | "plugins": [ 10 | "import", 11 | "react", 12 | "react-hooks" 13 | ], 14 | "env": { 15 | "node": true 16 | }, 17 | "settings": { 18 | "react": { 19 | "createClass": "createReactClass", 20 | "pragma": "React", 21 | "version": "detect", 22 | "flowVersion": "0.53" 23 | }, 24 | "propWrapperFunctions": [ 25 | "forbidExtraProps", 26 | {"property": "freeze", "object": "Object"}, 27 | {"property": "myFavoriteWrapper"} 28 | ] 29 | }, 30 | "rules": { 31 | "import/extensions": 2, 32 | 33 | "react/display-name": [2, { "ignoreTranspilerName": false }], 34 | "react/jsx-no-duplicate-props": 2, 35 | "react/jsx-no-undef": 2, 36 | "react/jsx-quotes": 0, 37 | "react/jsx-closing-bracket-location": [1, "tag-aligned"], 38 | "react/jsx-uses-react": 2, 39 | "react/jsx-uses-vars": 2, 40 | "react/no-did-mount-set-state": 2, 41 | "react/no-did-update-set-state": 2, 42 | "react/no-multi-comp": 2, 43 | "react/no-unknown-property": 2, 44 | "react/prop-types": 1, 45 | "react/react-in-jsx-scope": 2, 46 | "react/self-closing-comp": 0, 47 | "react/jsx-wrap-multilines": 2, 48 | "react/sort-comp": 0, 49 | 50 | "react-hooks/rules-of-hooks": "error", 51 | "react-hooks/exhaustive-deps": "warn", 52 | 53 | "quotes": [1, "single", "avoid-escape"], 54 | "jsx-quotes": 1, 55 | "comma-dangle": [1, "never"], 56 | "no-underscore-dangle": 0, 57 | "func-names": 0, 58 | "no-else-return": 2, 59 | "no-console": 1, 60 | "no-throw-literal": 0, 61 | "no-mixed-spaces-and-tabs": 2, 62 | "space-in-parens": [2, "never"], 63 | "space-infix-ops": 2, 64 | "space-before-blocks": 2, 65 | "id-length": 0, 66 | "camelcase": [2, {"properties": "always"}] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NodeJS 2 | node_modules 3 | 4 | # Development 5 | /dist 6 | /dist-electron 7 | -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@parcel/config-default"], 3 | "transformers": { 4 | "app.js": ["@futureportal/parcel-transformer-package-version", "..."], 5 | }, 6 | "reporters": ["...", "parcel-reporter-clean-dist", "parcel-reporter-static-files-copy"] 7 | } 8 | -------------------------------------------------------------------------------- /.posthtmlrc_electron: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "posthtml-expressions": {locals: { electron: true }} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.posthtmlrc_no_electron: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "posthtml-expressions": {locals: { electron: false }} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | options: 2 | merge-default-rules: true 3 | files: 4 | include: 'src/assets/styles/**/*.s+(a|c)ss' 5 | ignore: 6 | - '**/*_react-toggle.scss' 7 | rules: 8 | indentation: 9 | - 2 10 | - 11 | size: 4 12 | no-important: 13 | - 0 14 | no-vendor-prefixes: 15 | - 1 16 | - 17 | 'excluded-identifiers': 18 | - 'webkit' 19 | - 'moz' 20 | nesting-depth: 21 | - 1 22 | - 23 | 'max-depth': 3 24 | property-sort-order: 25 | - 1 26 | - 27 | order: 'recess' 28 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-recommended-scss", "stylelint-config-recess-order"], 3 | "rules": { 4 | "indentation": 4, 5 | "no-descending-specificity": null, 6 | "length-zero-no-unit": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016-present AG Projects 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU Affero General Public License as 5 | published by the Free Software Foundation, either version 3 of the 6 | License, or (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: all clean deploy deploy-test deploy-osx deploy-win dist dist-dev distclean watch serve lint pkg-osx pkg-win pkg-linux app-run 3 | 4 | all: dist 5 | 6 | deploy: dist 7 | echo `date +"%Y-%m-%d_%H:%M:%S"` > dist/.timestamp 8 | rm -f dist/js/*.map 9 | rm -f dist/assets/styles/*.map 10 | rsync -av --exclude .htaccess --exclude .well-known --delete dist/ -e 'ssh -A -J agp@ca-node02.dns-hosting.info:22' agp@10.208.120.103:/var/www/webrtc/ 11 | ssh -A -J agp@ca-node02.dns-hosting.info:22 agp@10.208.120.103 'sudo /root/sync-webrtc.sh' 12 | 13 | deploy-test: dist-dev 14 | echo `date +"%Y-%m-%d_%H:%M:%S"` > dist/.timestamp 15 | rsync -av --exclude .htaccess --delete dist/ -e 'ssh -A -J agp@ca-node02.dns-hosting.info:22' agp@10.208.120.103:/var/www/webrtc-test/ 16 | 17 | deploy-osx: 18 | #rsync -avz --progress dist-electron/Sylk*.dmg dist-electron/Sylk*.zip dist-electron/latest-mac*yml -e 'ssh -A -J agp@de-node02.dns-hosting.info:22' agp@10.208.118.4:/var/www/download/Sylk/ 19 | #ssh -A -J agp@de-node02.dns-hosting.info:22 agp@10.208.118.4 ssh agp@node08.dns-hosting.info 'sudo /root/symlink-sylk.sh' 20 | rsync -avz --progress dist-electron/Sylk*.dmg dist-electron/Sylk*.zip dist-electron/latest-mac*yml -e 'ssh -A -J agp@ca-node02.dns-hosting.info:22' agp@10.208.120.103:/var/www/download/Sylk/ 21 | ssh -A -J agp@ca-node02.dns-hosting.info:22 agp@10.208.120.103 'sudo /root/symlink-sylk.sh' 22 | 23 | deploy-win: 24 | #rsync -avz --progress dist-electron/Sylk*.exe dist-electron/latest.yml -e 'ssh -A -J agp@de-node02.dns-hosting.info:22' agp@10.208.118.4:/var/www/download/Sylk/ 25 | #ssh -A -J agp@de-node02.dns-hosting.info:22 agp@10.208.118.4 'sudo /root/symlink-sylk.sh' 26 | rsync -avz --progress dist-electron/Sylk*.exe dist-electron/latest.yml -e 'ssh -A -J agp@ca-node02.dns-hosting.info:22' agp@10.208.120.103:/var/www/download/Sylk/ 27 | ssh -A -J agp@ca-node02.dns-hosting.info:22 agp@10.208.120.103 'sudo /root/symlink-sylk.sh' 28 | 29 | deploy-linux: 30 | #rsync -avz --progress dist-electron/Sylk*.AppImage dist-electron/latest-linux*yml -e 'ssh -A -J agp@de-node02.dns-hosting.info:22' agp@10.208.118.4:/var/www/download/Sylk/ 31 | #ssh -A -J agp@de-node02.dns-hosting.info:22 agp@10.208.118.4 'sudo /root/symlink-sylk.sh' 32 | rsync -avz --progress dist-electron/Sylk*.AppImage dist-electron/latest-linux*yml -e 'ssh -A -J agp@ca-node02.dns-hosting.info:22' agp@10.208.120.103:/var/www/download/Sylk/ 33 | ssh -A -J agp@ca-node02.dns-hosting.info:22 agp@10.208.120.103 'sudo /root/symlink-sylk.sh' 34 | 35 | dist: 36 | npm run build 37 | 38 | dist-dev: 39 | npm run build-dev 40 | 41 | clean: 42 | rm -rf dist dist-electron app/www 43 | 44 | distclean: clean 45 | rm -rf node_modules app/node_modules 46 | 47 | watch: 48 | npm run dev 49 | 50 | serve: 51 | npm run serve 52 | 53 | lint: 54 | npm run lint 55 | 56 | electron: 57 | # TODO: use a different gulp task which doesn't browserify 58 | npm run electron 59 | 60 | pkg-osx: electron 61 | npm run build-osx 62 | 63 | pkg-win: electron 64 | npm run build-win 65 | 66 | pkg-linux: electron 67 | npm run build-linux 68 | 69 | app-run: electron 70 | npm start 71 | -------------------------------------------------------------------------------- /app/incomingWindow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const electron = require('electron'); 3 | const ipcRenderer = electron.ipcRenderer; 4 | 5 | document.addEventListener('keyup', onKeyUp); 6 | 7 | const div = document.createElement("div"); 8 | div.style.position = "absolute"; 9 | div.style.top = 0; 10 | div.style.left = 0; 11 | div.style.height = "32px"; 12 | div.style.width = "100%"; 13 | div.style.background = "transparent" 14 | div.style["-webkit-app-region"] = "drag"; 15 | document.body.appendChild(div); 16 | 17 | function onKeyUp(event) { 18 | switch (event.which) { 19 | case 27: 20 | // ESC 21 | ipcRenderer.send('button', 'decline'); 22 | break; 23 | case 13: 24 | let btn = document.getElementById('accept') || document.getElementById('audio'); 25 | ipcRenderer.send('button', btn.id); 26 | default: 27 | break; 28 | } 29 | }; 30 | 31 | ipcRenderer.on('updateContent', function(event, store) { 32 | document.querySelector('#app').innerHTML = store; 33 | 34 | for (let btn of document.getElementsByTagName('button')) { 35 | btn.addEventListener('click', (event) => { 36 | ipcRenderer.send('buttonClick', `${btn.id}`); 37 | }); 38 | } 39 | }); 40 | 41 | ipcRenderer.on('updateStyles', function(event, store) { 42 | const newStyleEl = document.createElement('style'); 43 | newStyleEl.appendChild(document.createTextNode(store)); 44 | document.head.appendChild(newStyleEl) 45 | }); 46 | 47 | 0; 48 | -------------------------------------------------------------------------------- /app/osxTitleBar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const titlebar = require('titlebar'); 4 | const electron = require('electron'); 5 | const ipc = electron.ipcRenderer; 6 | 7 | 8 | // setup a draggable area at the top of the window 9 | 10 | const div = document.createElement("div"); 11 | div.style.position = "absolute"; 12 | div.style.top = 0; 13 | div.style.left = 0; 14 | div.style.height = "50px"; 15 | div.style.width = "100%"; 16 | div.style["-webkit-app-region"] = "drag"; 17 | document.body.appendChild(div); 18 | 19 | 20 | // setup titlebar 21 | 22 | const titleBar = titlebar({disableFullScreen: true}); 23 | titleBar.element.style.position = "absolute"; 24 | titleBar.element.style.top = 0; 25 | titleBar.element.style.left = 0; 26 | titleBar.element.style.height = "50px"; 27 | titleBar.element.style["background-color"] = "transparent"; 28 | titleBar.element.style["transform-style"] = "preserve-3d"; 29 | titleBar.element.style["z-index"] = "9999"; 30 | 31 | const stopLights = titleBar.element.children[0]; 32 | stopLights.style.position = "relative"; 33 | stopLights.style.top = "50%"; 34 | stopLights.style.transform = "translateY(-50%)"; 35 | 36 | titleBar.appendTo(document.body); 37 | 38 | titleBar.on('close', function () { 39 | ipc.send('close') 40 | }) 41 | 42 | titleBar.on('minimize', function () { 43 | ipc.send('minimize') 44 | }) 45 | 46 | const linkElement = document.createElement('link'); 47 | linkElement.setAttribute('rel', 'stylesheet'); 48 | linkElement.setAttribute('type', 'text/css'); 49 | linkElement.setAttribute('href', 'data:text/css;charset=UTF-8,' + encodeURIComponent(".navbar-header { padding-left: 48.5px;}")); 50 | document.head.append(linkElement); 51 | 52 | // show / hide titlebar when going fullscreen 53 | 54 | document.addEventListener('webkitfullscreenchange', (e) => { 55 | if (document.webkitIsFullScreen) { 56 | titleBar.element.style.display = "none"; 57 | } else { 58 | titleBar.element.style.display = ""; 59 | } 60 | }, false); 61 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sylk-electron", 3 | "version": "3.7.0", 4 | "productName": "Sylk", 5 | "description": "WebRTC client", 6 | "author": "AG Projects ", 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 |
88 | 89 |
90 | 91 | 99 |
100 |
101 | 102 |
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 | 24 |
25 | :
26 |

The {props.wasCall === true ? 'call' : 'conference'} cannot be completed at this moment.
The reason was: {props.failureReason}

27 | 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 | 63 | 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 |
    41 | {entries} 42 |
    43 |
    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 |
    20 | {props.downloadFile(elem.filename)}}> 21 | {elem.filename} 22 | 23 | ({(elem.filesize / 1048576).toFixed(2)} MB) 24 |
    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 | 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 = ; 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 | 51 | 52 | View shortcuts 53 | 54 | 55 | Switch Devices 56 | 57 | 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 |
    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 | 111 | 112 | )} 113 | {props.export === true && ( 114 | 123 | )} 124 | {(step === 1 || props.export === true) && ( 125 | 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 |
    42 | 43 |
    44 | 45 | 46 |
    47 |
    48 |
    49 | 50 |
    51 |
    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 |
    9 |
    10 |
    11 |

    Copyright © AG Projects

    12 |
    13 |
    14 |
    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 | 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 | 121 | 124 | 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 |
      {answerButtons}
    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 |
    44 | 45 |
    46 | 47 | 48 |
    49 |
    50 |
    51 | 52 |
    53 |
    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 |
    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 |
    9 |
    10 |

    Sylk

    11 |
    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 | 129 | 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 | 24 | 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 | 71 | 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 |
    89 |
    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 | 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 |
    13 |
    14 | 15 | 20 |
    21 |
    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 | 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 | 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 |
    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 | 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 | 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 | 48 |
    49 |
    50 |
    51 | 52 | 53 | 54 | --------------------------------------------------------------------------------