diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index c72e4f3..b6d1a9b 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -8,54 +8,20 @@ jobs: - name: Check out Git repository uses: actions/checkout@v2.3.1 - # - name: Install Node.js, NPM and Yarn - # env: - # ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - # uses: actions/setup-node@v1.4.2 - # with: - # node-version: 14 - - # - name: yarn install from npmjs registry - # env: - # ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - # run: | - # npm config set registry https://registry.npmjs.org - # cd app/client - # yarn install --no-lockfile - # cd .. - # yarn install --no-lockfile - # cd .. - # yarn install --no-lockfile - - - name: configure app/client for yarn cache + - name: install yarn dependencies in app/client using cache uses: bahmutov/npm-install@v1.6.0 with: working-directory: ./app/client - - name: configure project root for yarn cache + - name: install yarn dependencies in ./ using cache uses: bahmutov/npm-install@v1.6.0 with: working-directory: ./ - - name: ./app/client yarn install - run: yarn install --frozen-lockfile - working-directory: ./app/client - - name: ./ yarn install - run: yarn install --frozen-lockfile - working-directory: ./ - - # - name: Configure private AWS npm registry and install packages from it - # env: - # ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - # if: ${{ failure() }} - # run: | - # npm config set registry https://packages.deskreen.com/ - # npm set //packages.deskreen.com/:_authToken="${{ secrets.NPMRC_USER_TOKEN }}" - # npm config set always-auth true - # cd app/client - # yarn install --frozen-lockfile - # cd ../.. - # yarn install --frozen-lockfile + - name: install yarn dependencies in ./app using cache + uses: bahmutov/npm-install@v1.6.0 + with: + working-directory: ./app - name: yarn build run: yarn build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e1ba063..4a10ff9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,37 +41,52 @@ jobs: - name: Checkout code uses: actions/checkout@v2.3.1 - - name: Install Node.js, NPM and Yarn - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - uses: actions/setup-node@v1.4.2 + - name: install yarn dependencies in app/client using cache + uses: bahmutov/npm-install@v1.6.0 with: - node-version: 14 + working-directory: ./app/client - - name: yarn install from npmjs registry - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - run: | - npm config set registry https://registry.npmjs.org - cd app/client - yarn install --no-lockfile - cd .. - yarn install --no-lockfile - cd .. - yarn install --no-lockfile + - name: install yarn dependencies in ./ using cache + uses: bahmutov/npm-install@v1.6.0 + with: + working-directory: ./ - - name: Configure private AWS npm registry and install packages from it + - name: install yarn dependencies in ./app using cache + uses: bahmutov/npm-install@v1.6.0 + with: + working-directory: ./app + + - name: yarn build env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - if: ${{ failure() }} - run: | - npm config set registry https://packages.deskreen.com/ - npm set //packages.deskreen.com/:_authToken="${{ secrets.NPMRC_USER_TOKEN }}" - npm config set always-auth true - cd app/client - yarn install --frozen-lockfile - cd ../.. - yarn install --frozen-lockfile + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: yarn build + + - name: yarn lint + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: yarn lint + + - name: yarn tsc + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: yarn tsc + + - name: yarn test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: yarn test + + - name: yarn build-ux + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: yarn build-ux + + - name: yarn test-ux + uses: GabrielBB/xvfb-action@v1.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + run: yarn test-ux - name: yarn package-ci env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 065a2dd..70525db 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,37 +16,20 @@ jobs: - name: Check out Git repository uses: actions/checkout@v2.3.1 - - name: Install Node.js, NPM and Yarn - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - uses: actions/setup-node@v1.4.2 + - name: install yarn dependencies in app/client using cache + uses: bahmutov/npm-install@v1.6.0 with: - node-version: 14 + working-directory: ./app/client - - name: yarn install from npmjs registry - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - run: | - npm config set registry https://registry.npmjs.org - cd app/client - yarn install --no-lockfile - cd .. - yarn install --no-lockfile - cd .. - yarn install --no-lockfile + - name: install yarn dependencies in ./ using cache + uses: bahmutov/npm-install@v1.6.0 + with: + working-directory: ./ - - name: Configure private AWS npm registry and install packages from it - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - if: ${{ failure() }} - run: | - npm config set registry https://packages.deskreen.com/ - npm set //packages.deskreen.com/:_authToken="${{ secrets.NPMRC_USER_TOKEN }}" - npm config set always-auth true - cd app/client - yarn install --frozen-lockfile - cd ../.. - yarn install --frozen-lockfile + - name: install yarn dependencies in ./app using cache + uses: bahmutov/npm-install@v1.6.0 + with: + working-directory: ./app # following step does code signing when `electron-builder --publish always` (look in package.json) - name: yarn package-ci diff --git a/app/api/config.ts b/app/api/config.ts index de96847..3cc61ab 100644 --- a/app/api/config.ts +++ b/app/api/config.ts @@ -4,7 +4,7 @@ let protocol; let port; if (!host && !protocol && !port) { - host = 'localhost'; + host = '127.0.0.1'; protocol = 'http'; port = 3131; // TODO: read port from signaling server api } diff --git a/app/app.global.css b/app/app.global.css index f722c23..aa0501a 100644 --- a/app/app.global.css +++ b/app/app.global.css @@ -143,6 +143,10 @@ body left: -40px !important; } +div.class-allow-device-to-connect-alert { + z-index: 9999; +} + /* ALLOW CONNECTION ALERT BLINK ANIMATION START */ div.class-allow-device-to-connect-alert > div.bp3-alert-body diff --git a/app/client/package.json b/app/client/package.json index aa5c71e..75d4c9e 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -25,6 +25,7 @@ "react-reveal": "^1.2.2", "react-scripts": "3.4.3", "react-spinners": "^0.9.0", + "react-test-renderer": "^17.0.1", "screenfull": "^5.0.2", "shortid": "^2.2.15", "socket.io-client": "^2.3.0", diff --git a/app/client/sonar-project.properties b/app/client/sonar-project.properties index db1b3a6..7486edd 100644 --- a/app/client/sonar-project.properties +++ b/app/client/sonar-project.properties @@ -1,7 +1,8 @@ sonar.projectKey=deskreen-viewer sonar.typescript.lcov.reportPaths=coverage/lcov.info sonar.sources=src -sonar.coverage.exclusions=src/**/*.spec.ts,src/**/*.spec.tsx,src/**/*.test.ts,src/**/*.test.tsx,src/serviceWorker.ts,src/index.tsx +sonar.cpd.exclusions=src/**/mocks/*,src/**/*.spec.ts,src/**/*.spec.tsx,src/**/*.test.ts,src/**/*.test.tsx,src/serviceWorker.ts,src/index.tsx +sonar.coverage.exclusions=src/**/mocks/*,src/**/*.spec.ts,src/**/*.spec.tsx,src/**/*.test.ts,src/**/*.test.tsx,src/serviceWorker.ts,src/index.tsx sonar.host.url=http://localhost:9000 sonar.login=e3b5f73b8778290f7074c40a4159c32b7f15a8e6 sonar.exclusions=src/serviceWorker.ts,node_modules/** diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index d9365b7..00f57d5 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -1,332 +1,9 @@ -import React, { - useEffect, - useState, - useRef, - useContext, - useCallback, -} from 'react'; -import { useTranslation } from 'react-i18next'; -import i18n from './config/i18n'; -import { H3, Position, Toaster } from '@blueprintjs/core'; -import { Grid, Row, Col } from 'react-flexbox-grid'; -import { findDOMNode } from 'react-dom'; -import ReactPlayer from 'react-player'; -import screenfull from 'screenfull'; -import Crypto from './utils/crypto'; -import './App.css'; -import PeerConnection from './features/PeerConnection'; -import VideoAutoQualityOptimizer from './features/VideoAutoQualityOptimizer'; -import ConnectingIndicator from './components/ConnectingIndicator'; -import MyDeviceInfoCard from './components/MyDeviceInfoCard'; -import { - DARK_UI_BACKGROUND, - LIGHT_UI_BACKGROUND, -} from './constants/styleConstants'; -import { AppContext } from './providers/AppContextProvider'; -import PlayerControlPanel from './components/PlayerControlPanel'; -import { VideoQuality } from './features/PeerConnection/VideoQualityEnum'; -import { REACT_PLAYER_WRAPPER_ID } from './constants/appConstants'; -import { TFunction } from 'i18next'; -import ErrorDialog from './components/ErrorDialog'; -import { ErrorMessage } from './components/ErrorDialog/ErrorMessageEnum'; - -const Fade = require('react-reveal/Fade'); -const Slide = require('react-reveal/Slide'); - -function getPromptContent(step: number, t: TFunction) { - switch (step) { - case 1: - return ( -

- {t( - 'Waiting for user to click ALLOW button on screen sharing device...' - )} -

- ); - case 2: - return

Connected!

; - case 3: - return ( -

- {t( - 'Wating for user to select source to share from screen sharing device...' - )} -

- ); - default: - return

Error occured :(

; - } -} +import React from 'react'; +import MainView from './containers/MainView'; function App() { - const { t } = useTranslation(); - const { isDarkTheme, setIsDarkThemeHook } = useContext(AppContext); - const [isErrorDialogOpen, setIsErrorDialogOpen] = useState(false); - - const [toaster, setToaster] = useState(); - - const refHandlers = { - toaster: (ref: Toaster) => { - setToaster(ref); - }, - }; - - const player = useRef(null); - const [promptStep, setPromptStep] = useState(1); - const [dialogErrorMessage, setDialogErrorMessage] = useState( - ErrorMessage.UNKNOWN_ERROR - ); - const [connectionIconType, setConnectionIconType] = useState< - 'feed' | 'feed-subscribed' - >('feed'); - const [myDeviceDetails, setMyDeviceDetails] = useState({ - myIP: '', - myOS: '', - myDeviceType: '', - myBrowser: '', - }); - - const [playing, setPlaying] = useState(true); - const [isFullScreenOn, setIsFullScreenOn] = useState(false); - const [url, setUrl] = useState(); - const [screenSharingSourceType, setScreenSharingSourceType] = useState< - 'screen' | 'window' - >('screen'); - const [isWithControls, setIsWithControls] = useState(!screenfull.isEnabled); - const [isShownTextPrompt, setIsShownTextPrompt] = useState(false); - const [isShownSpinnerIcon, setIsShownSpinnerIcon] = useState(false); - const [spinnerIconType, setSpinnerIconType] = useState< - 'desktop' | 'application' - >('desktop'); - const [videoQuality, setVideoQuality] = useState( - VideoQuality.Q_AUTO - ); - const [peer, setPeer] = useState(); - - const changeLanguage = (lng: string) => { - i18n.changeLanguage(lng); - }; - - useEffect(() => { - if (!peer) return; - if (!peer.isStreamStarted) return; - peer.setVideoQuality(videoQuality); - }, [videoQuality, peer]); - - useEffect(() => { - document.body.style.backgroundColor = isDarkTheme - ? DARK_UI_BACKGROUND - : LIGHT_UI_BACKGROUND; - }, [isDarkTheme]); - - useEffect(() => { - if (!peer) { - const _peer = new PeerConnection( - setUrl, - new Crypto(), - new VideoAutoQualityOptimizer(), - isDarkTheme, - setMyDeviceDetails, - () => { - setConnectionIconType('feed-subscribed'); - - setIsShownTextPrompt(false); - setIsShownTextPrompt(true); - setPromptStep(2); - - setTimeout(() => { - setIsShownTextPrompt(false); - setIsShownTextPrompt(true); - setPromptStep(3); - }, 2000); - }, - setScreenSharingSourceType, - setIsDarkThemeHook, - changeLanguage, - setDialogErrorMessage, - setIsErrorDialogOpen - ); - - setPeer(_peer); - - setTimeout(() => { - setIsShownTextPrompt(true); - }, 100); - } - }, [setIsDarkThemeHook, isDarkTheme, peer]); - - useEffect(() => { - // infinite use effect - setTimeout(() => { - setIsShownSpinnerIcon(!isShownSpinnerIcon); - setSpinnerIconType( - spinnerIconType === 'desktop' ? 'application' : 'desktop' - ); - }, 1500); - }, [isShownSpinnerIcon, spinnerIconType]); - - const handlePlayPause = useCallback(() => { - setPlaying(!playing); - }, [playing]); - - useEffect(() => { - if (url !== undefined) { - setTimeout(() => { - // @ts-ignore - document.querySelector('.container > .react-reveal').style.display = - 'none'; - }, 1000); - } - }, [url]); - - useEffect(() => { - if (promptStep === 3) { - // start infinite use effect - setIsShownSpinnerIcon(true); - } - }, [promptStep]); - return ( - - -
- - - -
- - - - - - -
- {getPromptContent(promptStep, t)} -
-
-
- -
-
- - - -
-
-
- setIsWithControls(isEnabled)} - isDefaultPlayerTurnedOn={isWithControls} - handleClickFullscreen={() => { - if (!screenfull.isEnabled) return; - // @ts-ignore Property 'request' does not exist on type '{ isEnabled: false; }'. - screenfull.request(findDOMNode(player.current)); - setIsFullScreenOn(!isFullScreenOn); - }} - handleClickPlayPause={handlePlayPause} - isPlaying={playing} - setVideoQuality={setVideoQuality} - selectedVideoQuality={videoQuality} - screenSharingSourceType={screenSharingSourceType} - toaster={toaster} - /> -
-
- -
- -
-
- - -
+ ); } diff --git a/app/client/src/api/config.ts b/app/client/src/api/config.ts index 0584c2d..ffe1463 100644 --- a/app/client/src/api/config.ts +++ b/app/client/src/api/config.ts @@ -1,4 +1,3 @@ -/* istanbul ignore file */ let host; let protocol; let port; diff --git a/app/client/src/api/generator.spec.ts b/app/client/src/api/generator.spec.ts new file mode 100644 index 0000000..d0dceba --- /dev/null +++ b/app/client/src/api/generator.spec.ts @@ -0,0 +1,34 @@ +import generator from './generator'; +import { TEST_PORT, TEST_HOST, TEST_PROTOCOL } from './mocks/generatorTestVariables'; + +// how to use local variables in jest mock to get rid of hoisting mocks to top most code block: +//stackoverflow.com/questions/44649699/service-mocked-with-jest-causes-the-module-factory-of-jest-mock-is-not-allowe +jest.mock('./config', () => { + const generatorTestVariables = require('./mocks/generatorTestVariables'); + + return { + host: generatorTestVariables.TEST_HOST, + protocol: generatorTestVariables.TEST_PROTOCOL, + port: generatorTestVariables.TEST_PORT, + }; +}); + + +describe('generator.ts', () => { + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe('when generator() is called properly', () => { + it('should produce correct string', () => { + const roomID = '333'; + + const result = generator(roomID); + + expect(result).toMatch( + `${TEST_PROTOCOL}://${TEST_HOST}:${TEST_PORT}/${roomID}` + ); + }); + }); +}); diff --git a/app/client/src/api/generator.ts b/app/client/src/api/generator.ts index 4debac7..1788bc4 100644 --- a/app/client/src/api/generator.ts +++ b/app/client/src/api/generator.ts @@ -1,4 +1,3 @@ -/* istanbul ignore file */ import config from './config'; export default (resourceName = '') => { diff --git a/app/client/src/api/mocks/generatorTestVariables.ts b/app/client/src/api/mocks/generatorTestVariables.ts new file mode 100644 index 0000000..f2e31ae --- /dev/null +++ b/app/client/src/api/mocks/generatorTestVariables.ts @@ -0,0 +1,3 @@ +export const TEST_PORT = '3232'; +export const TEST_HOST = '123.123.123.123'; +export const TEST_PROTOCOL = 'http'; diff --git a/app/client/src/components/ConnectingIndicator/LoadingSharingIcon.spec.tsx b/app/client/src/components/ConnectingIndicator/LoadingSharingIcon.spec.tsx new file mode 100644 index 0000000..d68cb0f --- /dev/null +++ b/app/client/src/components/ConnectingIndicator/LoadingSharingIcon.spec.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import LoadingSharingIcon from './LoadingSharingIcon'; + +jest.useFakeTimers(); + +it('should match exact snapshot', () => { + const subject = renderer.create( + <> + + + ); + + expect(subject).toMatchSnapshot(); +}); diff --git a/app/client/src/components/ConnectingIndicator/LoadingSharingIcon.tsx b/app/client/src/components/ConnectingIndicator/LoadingSharingIcon.tsx new file mode 100644 index 0000000..5517664 --- /dev/null +++ b/app/client/src/components/ConnectingIndicator/LoadingSharingIcon.tsx @@ -0,0 +1,65 @@ +import React, { useContext } from 'react'; +import { Icon } from '@blueprintjs/core'; +import { Col, Row } from 'react-flexbox-grid'; +import PropagateLoader from 'react-spinners/PropagateLoader'; +import { AppContext } from '../../providers/AppContextProvider'; + +const Fade = require('react-reveal/Fade'); + +interface SelectSharingIconProps { + loadingSharingIconType: LoadingSharingIconType; + isShownLoadingSharingIcon: boolean; +} + +function LoadingSharingIcon(props: SelectSharingIconProps) { + const { isDarkTheme } = useContext(AppContext); + + const { + loadingSharingIconType: selectingSharingIconType, + isShownLoadingSharingIcon: isShownSelectingSharingIcon, + } = props; + + return ( + + + + + + + + + + + + + + + + + ); +} + +export default LoadingSharingIcon; diff --git a/app/client/src/components/ConnectingIndicator/SelectSharingIcon.tsx b/app/client/src/components/ConnectingIndicator/SelectSharingIcon.tsx deleted file mode 100644 index 33bbf60..0000000 --- a/app/client/src/components/ConnectingIndicator/SelectSharingIcon.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useContext } from 'react'; -import { Icon } from '@blueprintjs/core'; -import { Col, Row } from 'react-flexbox-grid'; -import PropagateLoader from 'react-spinners/PropagateLoader'; -import { AppContext } from '../../providers/AppContextProvider'; - -const Fade = require('react-reveal/Fade'); - -interface SelectSharingIconProps { - selectingSharingIconType: 'desktop' | 'application'; - isShownSelectingSharingIcon: boolean; -} - -function SelectSharingIcon(props: SelectSharingIconProps) { - const { isDarkTheme } = useContext(AppContext); - - const { selectingSharingIconType, isShownSelectingSharingIcon } = props; - - return ( - - - - - - - - - - - - - - - - - ); -} - -export default SelectSharingIcon; diff --git a/app/client/src/components/ConnectingIndicator/__snapshots__/LoadingSharingIcon.spec.tsx.snap b/app/client/src/components/ConnectingIndicator/__snapshots__/LoadingSharingIcon.spec.tsx.snap new file mode 100644 index 0000000..3b04745 --- /dev/null +++ b/app/client/src/components/ConnectingIndicator/__snapshots__/LoadingSharingIcon.spec.tsx.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should match exact snapshot 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + desktop + + + + +
+
+
+
+
+`; diff --git a/app/client/src/components/ConnectingIndicator/index.tsx b/app/client/src/components/ConnectingIndicator/index.tsx index 443f094..0b19a7d 100644 --- a/app/client/src/components/ConnectingIndicator/index.tsx +++ b/app/client/src/components/ConnectingIndicator/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Text } from '@blueprintjs/core'; import { Row } from 'react-flexbox-grid'; import ConnectingIndicatorIcon from './ConnectingIndicatorIcon'; -import SelectSharingIcon from './SelectSharingIcon'; +import LoadingSharingIcon from './LoadingSharingIcon'; const basePulsingCircleStyles = { borderRadius: '100%', @@ -18,9 +18,9 @@ const basePulsingCircleStyles = { function getConnectingStepContent( currentStep: number, - connectionIconType: 'feed' | 'feed-subscribed', - selectingSharingIconType: 'desktop' | 'application', - isShownSelectingSharingIcon: boolean, + connectionIconType: ConnectionIconType, + loadingSharingIconType: LoadingSharingIconType, + isShownLoadingSharingIcon: boolean, ) { const pulsingCircle1Styles = { @@ -68,9 +68,9 @@ function getConnectingStepContent( ); case 3: return ( - ); default: @@ -80,9 +80,9 @@ function getConnectingStepContent( interface ConnectingIndicatorProps { currentStep: number; - connectionIconType: 'feed' | 'feed-subscribed'; + connectionIconType: ConnectionIconType; isShownSelectingSharingIcon: boolean; - selectingSharingIconType: 'desktop' | 'application'; + selectingSharingIconType: LoadingSharingIconType; } function ConnectingIndicator(props: ConnectingIndicatorProps) { diff --git a/app/client/src/components/ErrorDialog/ErrorMessageEnum.ts b/app/client/src/components/ErrorDialog/ErrorMessageEnum.ts index daa64f4..a6b296a 100644 --- a/app/client/src/components/ErrorDialog/ErrorMessageEnum.ts +++ b/app/client/src/components/ErrorDialog/ErrorMessageEnum.ts @@ -1,6 +1,6 @@ export enum ErrorMessage { UNKNOWN_ERROR = 'An unknonw error uccured.', DENY_TO_CONNECT = 'You were not allowed to connect.', - DICONNECTED = 'You were disconnected.', + DISCONNECTED = 'You were disconnected.', NOT_ALLOWED = 'You were not allowed to connect.', } diff --git a/app/client/src/components/PlayerControlPanel/__snapshots__/index.spec.tsx.snap b/app/client/src/components/PlayerControlPanel/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000..48178af --- /dev/null +++ b/app/client/src/components/PlayerControlPanel/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,393 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should match exact snapshot 1`] = ` +
+
+
+ + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + + + + +
+ + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+`; diff --git a/app/client/src/components/PlayerControlPanel/handlePlayerToggleFullscreen.ts b/app/client/src/components/PlayerControlPanel/handlePlayerToggleFullscreen.ts new file mode 100644 index 0000000..f4e4d50 --- /dev/null +++ b/app/client/src/components/PlayerControlPanel/handlePlayerToggleFullscreen.ts @@ -0,0 +1,33 @@ +/* istanbul ignore file */ + +// IMPORTANT! leave upper blank line so this file is ignored for coverage!!! More on this issue here +// https://github.com/facebook/create-react-app/issues/6106#issuecomment-550076629 +import { REACT_PLAYER_WRAPPER_ID } from "../../constants/appConstants"; + +export default () => { + const player = document.querySelector( + `#${REACT_PLAYER_WRAPPER_ID} > video` + ); + if (!player) return; + // @ts-ignore + if (player.requestFullScreen) { + // @ts-ignore + player.requestFullScreen(); + // @ts-ignore + } else if (player.webkitRequestFullScreen) { + // @ts-ignore + player.webkitRequestFullScreen(); + // @ts-ignore + } else if (player.mozRequestFullScreen) { + // @ts-ignore + player.mozRequestFullScreen(); + // @ts-ignore + } else if (player.msRequestFullscreen) { + // @ts-ignore + player.msRequestFullscreen(); + // @ts-ignore + } else if (player.webkitEnterFullscreen) { + // @ts-ignore + player.webkitEnterFullscreen(); //for iphone this code worked + } +}; diff --git a/app/client/src/components/PlayerControlPanel/index.spec.tsx b/app/client/src/components/PlayerControlPanel/index.spec.tsx new file mode 100644 index 0000000..8ac582a --- /dev/null +++ b/app/client/src/components/PlayerControlPanel/index.spec.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import PlayerControlPanel from '.'; +import { VideoQuality } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum'; + +jest.useFakeTimers(); + +it('should match exact snapshot', () => { + const subject = renderer.create( + <> + {}} + isPlaying + isDefaultPlayerTurnedOn + handleClickFullscreen={() => {}} + handleClickPlayPause={() => {}} + setVideoQuality={() => {}} + selectedVideoQuality={VideoQuality.Q_100_PERCENT} + screenSharingSourceType={'screen'} + toaster={undefined} + /> + + ); + expect(subject).toMatchSnapshot(); +}); diff --git a/app/client/src/components/PlayerControlPanel/index.tsx b/app/client/src/components/PlayerControlPanel/index.tsx index 1479367..a09a280 100644 --- a/app/client/src/components/PlayerControlPanel/index.tsx +++ b/app/client/src/components/PlayerControlPanel/index.tsx @@ -1,9 +1,8 @@ -import React, { - useEffect, - useMemo, - useState, - useCallback, -} from 'react'; +/* istanbul ignore file */ + +// IMPORTANT! leave upper blank line so this file is ignored for coverage!!! More on this issue here +// https://github.com/facebook/create-react-app/issues/6106#issuecomment-550076629 +import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { Alignment, Button, @@ -26,8 +25,9 @@ import DeskreenIconPNG from '../../images/deskreen_logo_128x128.png'; import RedHeartTwemojiPNG from '../../images/red_heart_2764_twemoji_120x120.png'; import { Col, Row } from 'react-flexbox-grid'; import screenfull from 'screenfull'; -import { VideoQuality } from '../../features/PeerConnection/VideoQualityEnum'; -import { REACT_PLAYER_WRAPPER_ID } from '../../constants/appConstants'; +import { VideoQuality } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum'; +import handlePlayerToggleFullscreen from './handlePlayerToggleFullscreen'; +import initScreenfullOnChange from './initScreenfullOnChange'; const videoQualityButtonStyle: React.CSSProperties = { width: '100%', @@ -42,12 +42,11 @@ interface PlayerControlPanelProps { handleClickPlayPause: () => void; setVideoQuality: (q: VideoQuality) => void; selectedVideoQuality: VideoQuality; - screenSharingSourceType: 'screen' | 'window'; + screenSharingSourceType: ScreenSharingSourceType; toaster: undefined | Toaster; } function PlayerControlPanel(props: PlayerControlPanelProps) { - const { isPlaying, onSwitchChangedCallback, @@ -65,40 +64,11 @@ function PlayerControlPanel(props: PlayerControlPanelProps) { const [isFullScreenOn, setIsFullScreenOn] = useState(false); useEffect(() => { - if (!screenfull.isEnabled) return; - // @ts-ignore - screenfull.on('change', () => { - // @ts-ignore - setIsFullScreenOn(screenfull.isFullscreen); - }); + initScreenfullOnChange(setIsFullScreenOn); }, []); const handleClickFullscreenWhenDefaultPlayerIsOn = useCallback(() => { - const player = document.querySelector( - `#${REACT_PLAYER_WRAPPER_ID} > video` - ); - if (!player) return; - // @ts-ignore - if (player.requestFullScreen) { - // @ts-ignore - player.requestFullScreen(); - // @ts-ignore - } else if (player.webkitRequestFullScreen) { - // @ts-ignore - player.webkitRequestFullScreen(); - // @ts-ignore - } else if (player.mozRequestFullScreen) { - // @ts-ignore - player.mozRequestFullScreen(); - // @ts-ignore - } else if (player.msRequestFullscreen) { - // @ts-ignore - player.msRequestFullscreen(); - // @ts-ignore - } else if (player.webkitEnterFullscreen) { - // @ts-ignore - player.webkitEnterFullscreen(); //for iphone this code worked - } + handlePlayerToggleFullscreen(); }, []); return ( @@ -113,7 +83,12 @@ function PlayerControlPanel(props: PlayerControlPanelProps) { + + + + + + + +
+
+
+
+
+ +
+ + + + + + + + + +
+ + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+`; diff --git a/app/client/src/containers/PlayerView/index.spec.tsx b/app/client/src/containers/PlayerView/index.spec.tsx new file mode 100644 index 0000000..f91418b --- /dev/null +++ b/app/client/src/containers/PlayerView/index.spec.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import PlayerView from '.'; +import { VideoQuality } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum'; + +jest.useFakeTimers(); + +it('should match exact snapshot', () => { + const subject = renderer.create( + <> + {}} + isFullScreenOn={false} + setIsFullScreenOn={() => {}} + handlePlayPause={() => {}} + isPlaying={false} + setVideoQuality={() => {}} + videoQuality={VideoQuality.Q_100_PERCENT} + screenSharingSourceType={'screen'} + streamUrl={undefined} + /> + + ); + expect(subject).toMatchSnapshot(); +}); diff --git a/app/client/src/containers/PlayerView/index.tsx b/app/client/src/containers/PlayerView/index.tsx new file mode 100644 index 0000000..f0cf283 --- /dev/null +++ b/app/client/src/containers/PlayerView/index.tsx @@ -0,0 +1,116 @@ +import { Position, Toaster } from '@blueprintjs/core'; +import React, { useRef, useState } from 'react'; +import ReactPlayer from 'react-player'; +import screenfull from 'screenfull'; +import PlayerControlPanel from '../../components/PlayerControlPanel'; +import { + COMPARISON_CANVAS_ID, + REACT_PLAYER_WRAPPER_ID, +} from '../../constants/appConstants'; +import { VideoQuality } from '../../features/VideoAutoQualityOptimizer/VideoQualityEnum'; + +interface PlayerViewProps { + isWithControls: boolean; + setIsWithControls: (_: boolean) => void; + isFullScreenOn: boolean; + setIsFullScreenOn: (_: boolean) => void; + handlePlayPause: () => void; + isPlaying: boolean; + setVideoQuality: (_: VideoQuality) => void; + videoQuality: VideoQuality; + screenSharingSourceType: ScreenSharingSourceType; + streamUrl: undefined | MediaStream; +} + +function PlayerView(props: PlayerViewProps) { + const { + screenSharingSourceType, + setIsWithControls, + isWithControls, + isFullScreenOn, + setIsFullScreenOn, + handlePlayPause, + isPlaying, + setVideoQuality, + videoQuality, + streamUrl, + } = props; + + const player = useRef(null); + const [toaster, setToaster] = useState(); + + const refHandlers = { + toaster: (ref: Toaster) => { + setToaster(ref); + }, + }; + + return ( +
+ setIsWithControls(isEnabled)} + isDefaultPlayerTurnedOn={isWithControls} + handleClickFullscreen={() => { + if (!screenfull.isEnabled) return; + const playerElement = document.querySelector(`#${REACT_PLAYER_WRAPPER_ID}`); + if (!playerElement) return; + screenfull.request(playerElement); + setIsFullScreenOn(!isFullScreenOn); + }} + handleClickPlayPause={handlePlayPause} + isPlaying={isPlaying} + setVideoQuality={setVideoQuality} + selectedVideoQuality={videoQuality} + screenSharingSourceType={screenSharingSourceType} + toaster={toaster} + /> +
+
+ +
+ +
+ +
+ ); +} + +export default PlayerView; diff --git a/app/client/src/features/PeerConnection/NullUser.ts b/app/client/src/features/PeerConnection/NullUser.ts new file mode 100644 index 0000000..e220d15 --- /dev/null +++ b/app/client/src/features/PeerConnection/NullUser.ts @@ -0,0 +1 @@ +export default { username: '', publicKey: '', privateKey: '' }; diff --git a/app/client/src/features/PeerConnection/PartnerPeerUser.d.ts b/app/client/src/features/PeerConnection/PartnerPeerUser.d.ts new file mode 100644 index 0000000..b2aebca --- /dev/null +++ b/app/client/src/features/PeerConnection/PartnerPeerUser.d.ts @@ -0,0 +1,4 @@ +interface PartnerPeerUser { + username: string; + publicKey: string; +} diff --git a/app/client/src/features/PeerConnection/PeerConnectionUIHandler.ts b/app/client/src/features/PeerConnection/PeerConnectionUIHandler.ts new file mode 100644 index 0000000..79bce13 --- /dev/null +++ b/app/client/src/features/PeerConnection/PeerConnectionUIHandler.ts @@ -0,0 +1,41 @@ +import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum'; + +export default class PeerConnectionUIHandler { + isDarkTheme: boolean; + + setMyDeviceDetails: (details: DeviceDetails) => void; + + hostAllowedToConnectCallback: () => void; + + setScreenSharingSourceTypeCallback: (s: ScreenSharingSourceType) => void; + + setIsDarkThemeCallback: (val: boolean) => void; + + setAppLanguageCallback: (newLang: string) => void; + + setDialogErrorMessageCallback: (message: ErrorMessage) => void; + + setIsErrorDialogOpen: (val: boolean) => void; + + errorDialogMessage = ErrorMessage.UNKNOWN_ERROR; + + constructor( + isDarkTheme: boolean, + setMyDeviceDetails: (details: DeviceDetails) => void, + hostAllowedToConnectCallback: () => void, + setScreenSharingSourceTypeCallback: (s: ScreenSharingSourceType) => void, + setIsDarkThemeCallback: (val: boolean) => void, + setAppLanguageCallback: (newLang: string) => void, + setDialogErrorMessageCallback: (message: ErrorMessage) => void, + setIsErrorDialogOpen: (val: boolean) => void + ) { + this.isDarkTheme = isDarkTheme; + this.hostAllowedToConnectCallback = hostAllowedToConnectCallback; + this.setMyDeviceDetails = setMyDeviceDetails; + this.setScreenSharingSourceTypeCallback = setScreenSharingSourceTypeCallback; + this.setIsDarkThemeCallback = setIsDarkThemeCallback; + this.setAppLanguageCallback = setAppLanguageCallback; + this.setDialogErrorMessageCallback = setDialogErrorMessageCallback; + this.setIsErrorDialogOpen = setIsErrorDialogOpen; + } +} diff --git a/app/client/src/features/PeerConnection/ReceiveEncryptedMessagePayload.d.ts b/app/client/src/features/PeerConnection/ReceiveEncryptedMessagePayload.d.ts new file mode 100644 index 0000000..5947317 --- /dev/null +++ b/app/client/src/features/PeerConnection/ReceiveEncryptedMessagePayload.d.ts @@ -0,0 +1,6 @@ +interface ReceiveEncryptedMessagePayload { + payload: string; + signature: string; + iv: string; + keys: { sessionKey: string; signingKey: string }[]; +} diff --git a/app/client/src/features/PeerConnection/errors/PeerConnectionPartnerIsNotDefinedError.ts b/app/client/src/features/PeerConnection/errors/PeerConnectionPartnerIsNotDefinedError.ts new file mode 100644 index 0000000..f852dd5 --- /dev/null +++ b/app/client/src/features/PeerConnection/errors/PeerConnectionPartnerIsNotDefinedError.ts @@ -0,0 +1,7 @@ +export default class PeerConnectionPartnerIsNotDefinedError extends Error { + constructor() { + super('partner should be defined!'); + // Set the prototype explicitly. + Object.setPrototypeOf(this, PeerConnectionPartnerIsNotDefinedError.prototype); + } +} diff --git a/app/client/src/features/PeerConnection/errors/PeerConnectionPeerIsNullError.ts b/app/client/src/features/PeerConnection/errors/PeerConnectionPeerIsNullError.ts new file mode 100644 index 0000000..46b307d --- /dev/null +++ b/app/client/src/features/PeerConnection/errors/PeerConnectionPeerIsNullError.ts @@ -0,0 +1,7 @@ +export default class PeerConnectionPeerIsNullError extends Error { + constructor() { + super('peer of PeerConnection should not be null!'); + // Set the prototype explicitly. + Object.setPrototypeOf(this, PeerConnectionPeerIsNullError.prototype); + } +} diff --git a/app/client/src/features/PeerConnection/errors/PeerConnectionSocketNotDefined.ts b/app/client/src/features/PeerConnection/errors/PeerConnectionSocketNotDefined.ts new file mode 100644 index 0000000..61e6ba3 --- /dev/null +++ b/app/client/src/features/PeerConnection/errors/PeerConnectionSocketNotDefined.ts @@ -0,0 +1,7 @@ +export default class PeerConnectionSocketNotDefined extends Error { + constructor() { + super('socket should be defined!'); + // Set the prototype explicitly. + Object.setPrototypeOf(this, PeerConnectionSocketNotDefined.prototype); + } +} diff --git a/app/client/src/features/PeerConnection/errors/PeerConnectionUserIsNotDefinedError.ts b/app/client/src/features/PeerConnection/errors/PeerConnectionUserIsNotDefinedError.ts new file mode 100644 index 0000000..83f00dc --- /dev/null +++ b/app/client/src/features/PeerConnection/errors/PeerConnectionUserIsNotDefinedError.ts @@ -0,0 +1,7 @@ +export default class PeerConnectionUserIsNotDefinedError extends Error { + constructor() { + super('user should be defined!'); + // Set the prototype explicitly. + Object.setPrototypeOf(this, PeerConnectionUserIsNotDefinedError.prototype); + } +} diff --git a/app/client/src/features/PeerConnection/index.spec.ts b/app/client/src/features/PeerConnection/index.spec.ts new file mode 100644 index 0000000..362f54f --- /dev/null +++ b/app/client/src/features/PeerConnection/index.spec.ts @@ -0,0 +1,237 @@ +jest.useFakeTimers(); + +jest.mock('../../utils/crypto.ts'); +jest.mock('../VideoAutoQualityOptimizer'); +jest.mock('simple-peer'); +jest.mock('./setAndShowErrorDialogMessage'); +jest.mock('./peerConnectionReceiveEncryptedMessage'); + +import SimplePeer from 'simple-peer'; +import PeerConnection from '.'; +import { VIDEO_QUALITY_TO_DECIMAL } from '../../constants/appConstants'; +import Crypto from '../../utils/crypto'; +import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer'; +import { VideoQuality } from '../VideoAutoQualityOptimizer/VideoQualityEnum'; +import NullUser from './NullUser'; +import PeerConnectionPartnerIsNotDefinedError from './errors/PeerConnectionPartnerIsNotDefinedError'; +import PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined'; +import PeerConnectionUIHandler from './PeerConnectionUIHandler'; +import PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError'; +import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage'; +import { prepareDataMessageToChangeQuality } from './simplePeerDataMessages'; +import peerConnectionReceiveEncryptedMessage from './peerConnectionReceiveEncryptedMessage'; + +const SEND_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD = { + type: 'DUMMY_MESSAGE', + payload: {}, +}; + +const RECEIVE_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD = { + payload: '', + signature: '', + iv: '', + keys: [{ sessionKey: '', signingKey: '' }], +}; + +describe('PeerConnection class', () => { + let peerConnection: PeerConnection; + + beforeEach(() => { + peerConnection = new PeerConnection( + '123', + jest.fn(), + new Crypto(), + new VideoAutoQualityOptimizer(), + new PeerConnectionUIHandler( + true, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn() + ) + ); + peerConnection.peer = new SimplePeer(); + peerConnection.peer.send = jest.fn(); + + const listeners = new Map(); + + peerConnection.socket = { + on: jest.fn().mockImplementation((s: string, callback: any) => { + if (!listeners.has(s)) { + listeners.set(s, []); + } + listeners.get(s)?.push(callback); + }), + emit: jest.fn().mockImplementation((s: string, any: any) => { + listeners.forEach((callbacks, key) => { + callbacks.forEach((callback) => { + if (key === s) { + callback(any); + } + }); + }); + }), + }; + + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe('when new PeerConnection is created with not corrent roomId', () => { + it('should change UI accordingly and notify user that error occured', () => { + peerConnection = new PeerConnection( + '', + jest.fn(), + new Crypto(), + new VideoAutoQualityOptimizer(), + new PeerConnectionUIHandler( + true, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn() + ) + ); + + expect(setAndShowErrorDialogMessage).toBeCalled(); + }); + }); + + describe('when new PeerConnection was created properly', () => { + describe('when setVideoQuality is called properly', () => { + it('should set video quality properly', () => { + peerConnection.videoQualityChangedCallback = jest.fn(); + const newVideoQuality = VideoQuality.Q_100_PERCENT; + peerConnection.setVideoQuality(newVideoQuality); + + expect(peerConnection.videoQualityChangedCallback).toBeCalled(); + expect(peerConnection.videoQuality).toBe(newVideoQuality); + }); + }); + + describe('when videoQualityChangedCallback is called and videoQuality of peer is VideoQuality.Q_AUTO', () => { + it('should call peerConnection.peer.send with proper message', () => { + peerConnection.videoQuality = VideoQuality.Q_AUTO; + peerConnection.videoQualityChangedCallback(); + + expect(peerConnection.peer?.send).toBeCalledWith( + prepareDataMessageToChangeQuality(1) + ); + }); + }); + + describe('when videoQualityChangedCallback is called and videoQuality of peer is NOT VideoQuality.Q_AUTO', () => { + it('should call peerConnection.peer.send with proper message', () => { + peerConnection.videoQuality = VideoQuality.Q_25_PERCENT; + peerConnection.videoQualityChangedCallback(); + + expect(peerConnection.peer?.send).toBeCalledWith( + prepareDataMessageToChangeQuality( + VIDEO_QUALITY_TO_DECIMAL[peerConnection.videoQuality] + ) + ); + }); + }); + + describe('when initApp is called when socket is not defined', () => { + it('should throw an appropriate error', () => { + peerConnection.socket = undefined; + try { + peerConnection.initApp(NullUser, ''); + fail('it should have thrown an error here'); + } catch (e) { + expect(e).toEqual(new PeerConnectionSocketNotDefined()); + } + }); + }); + + describe('when initApp is called properly', () => { + it('should call emit on socket with USER_ENTER', () => { + peerConnection.initApp(NullUser, ''); + + expect(peerConnection.socket.emit).toBeCalledWith( + 'USER_ENTER', + expect.anything() + ); + }); + }); + + describe('when sendEncryptedMessage is called when socket is NOT defined', () => { + it('should throw an appropriate error', () => { + peerConnection.socket = undefined; + try { + peerConnection.sendEncryptedMessage( + SEND_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD + ); + fail('it should have thrown an error here'); + } catch (e) { + expect(e).toEqual(new PeerConnectionSocketNotDefined()); + } + }); + }); + + describe('when sendEncryptedMessage is called when user is NOT defined', () => { + it('should throw an appropriate error', () => { + try { + peerConnection.sendEncryptedMessage( + SEND_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD + ); + fail('it should have thrown an error here'); + } catch (e) { + expect(e).toEqual(new PeerConnectionUserIsNotDefinedError()); + } + }); + }); + + describe('when sendEncryptedMessage is called when partner is NOT defined', () => { + it('should throw an appropriate error', () => { + peerConnection.user = { + username: 'af', + privateKey: 'af', + publicKey: 'af', + }; + try { + peerConnection.sendEncryptedMessage( + SEND_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD + ); + fail('it should have thrown an error here'); + } catch (e) { + expect(e).toEqual(new PeerConnectionPartnerIsNotDefinedError()); + } + }); + }); + + describe('when receiveEncryptedMessage is called', () => { + it('should call peerConnectionReceiveEncryptedMessage callback', () => { + peerConnection.receiveEncryptedMessage( + RECEIVE_ENCRYPTED_MESSAGE_DUMMY_PAYLOAD + ); + + expect(peerConnectionReceiveEncryptedMessage).toBeCalled(); + }); + }); + + describe('when createUserAndInitSocket is called and when socket is NOT defined', () => { + it('should throw an appropriate error', () => { + peerConnection.socket = undefined; + try { + peerConnection.createUserAndInitSocket(); + fail('it should have thrown an error here'); + } catch (e) { + expect(e).toEqual(new PeerConnectionSocketNotDefined()); + } + }); + }); + }); +}); diff --git a/app/client/src/features/PeerConnection/index.ts b/app/client/src/features/PeerConnection/index.ts index fe75b73..1011058 100644 --- a/app/client/src/features/PeerConnection/index.ts +++ b/app/client/src/features/PeerConnection/index.ts @@ -1,25 +1,26 @@ import shortId from 'shortid'; -// import pixelmatch from 'pixelmatch'; import SimplePeer from 'simple-peer'; import { UAParser } from 'ua-parser-js'; import { connect as connectSocket } from '../../utils/socket'; -import { - prepare as prepareMessage, - process as processMessage, -} from '../../utils/message'; +import { prepare as prepareMessage } from '../../utils/message'; import setSdpMediaBitrate from './setSdpMediaBitrate'; import Crypto from '../../utils/crypto'; import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer'; -import { - getBrowserFromUAParser, - getDeviceTypeFromUAParser, - getOSFromUAParser, -} from '../../utils/userAgentParserHelpers'; -import { VideoQuality } from './VideoQualityEnum'; -import prepareDataMessageToChangeQuality from './prepareDataMessageToChangeQuality'; +import { VideoQuality } from '../VideoAutoQualityOptimizer/VideoQualityEnum'; +import { prepareDataMessageToChangeQuality } from './simplePeerDataMessages'; import { VIDEO_QUALITY_TO_DECIMAL } from './../../constants/appConstants'; -import prepareDataMessageToGetSharingSourceType from './prepareDataMessageToGetSharingSourceType'; import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum'; +import areWeTestingWithJest from '../../utils/areWeTestingWithJest'; +import peerConnectionHandleSocket from './peerConnectionHandleSocket'; +import peerConnectionHandlePeer from './peerConnectionHandlePeer'; +import peerConnectionReceiveEncryptedMessage from './peerConnectionReceiveEncryptedMessage'; +import startSocketConnectedCheckingLoop from './startSocketConnectedCheckingLoop'; +import NullUser from './NullUser'; +import PeerConnectionUIHandler from './PeerConnectionUIHandler'; +import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage'; +import PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined'; +import PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError'; +import PeerConnectionPartnerIsNotDefinedError from './errors/PeerConnectionPartnerIsNotDefinedError'; interface LocalPeerUser { username: string; @@ -27,25 +28,11 @@ interface LocalPeerUser { publicKey: string; } -interface PartnerPeerUser { - username: string; - publicKey: string; -} - -const nullUser = { username: '', publicKey: '', privateKey: '' }; - interface SendEncryptedMessagePayload { type: string; payload: Record; } -interface ReceiveEncryptedMessagePayload { - payload: string; - signature: string; - iv: string; - keys: { sessionKey: string; signingKey: string }[]; -} - export default class PeerConnection { roomId: string; @@ -53,33 +40,22 @@ export default class PeerConnection { crypto: Crypto; - user: LocalPeerUser = nullUser; + user: LocalPeerUser = NullUser; - partner: PartnerPeerUser = nullUser; + partner: PartnerPeerUser = NullUser; peer: null | SimplePeer.Instance = null; - myIP = ''; + myDeviceDetails: DeviceDetails = { + myIP: '', + myOS: '', + myDeviceType: '', + myBrowser: '', + }; - myOS: any; + setUrlCallback: (url: any) => void; - myDeviceType: any; - - myBrowser: any; - - mousePos: any; - - setUrlCallback: any; - - private uaParser: UAParser; - - canvas: any; - - video: any; - - prevFrame: any; - - largeMismatchFramesCount: number; + uaParser: UAParser; screenSharingSourceType: string | undefined = undefined; @@ -87,74 +63,32 @@ export default class PeerConnection { videoAutoQualityOptimizer: VideoAutoQualityOptimizer; - isDarkTheme: boolean; - isStreamStarted: boolean = false; - setMyDeviceDetails: (details: DeviceDetails) => void; - - hostAllowedToConnectCallback: () => void; - - setScreenSharingSourceTypeCallback: (s: 'screen' | 'window') => void; - - setIsDarkThemeCallback: (val: boolean) => void; - - setAppLanguageCallback: (newLang: string) => void; - - setDialogErrorMessageCallback: (message: ErrorMessage) => void; - - setIsErrorDialogOpen: (val: boolean) => void; - - errorDialogMessage = ErrorMessage.UNKNOWN_ERROR; + UIHandler: PeerConnectionUIHandler; constructor( - setUrlCallback: any, + roomId: string, + setUrlCallback: (url: any) => void, crypto: Crypto, videoAutoQualityOptimizer: VideoAutoQualityOptimizer, - isDarkTheme: boolean, - setMyDeviceDetailsCallback: (details: DeviceDetails) => void, - hostAllowedToConnectCallback: () => void, - setScreenSharingSourceTypeCallback: (s: 'screen' | 'window') => void, - setIsDarkThemeCallback: (val: boolean) => void, - setAppLanguageCallback: (newLang: string) => void, - setDialogErrorMessageCallback: (message: ErrorMessage) => void, - setIsErrorDialogOpen: (val: boolean) => void + UIHandler: PeerConnectionUIHandler ) { this.setUrlCallback = setUrlCallback; this.crypto = crypto; this.videoAutoQualityOptimizer = videoAutoQualityOptimizer; - this.isDarkTheme = isDarkTheme; - this.setMyDeviceDetails = setMyDeviceDetailsCallback; - this.hostAllowedToConnectCallback = hostAllowedToConnectCallback; - this.roomId = encodeURI(window.location.pathname.replace('/', '')); + this.UIHandler = UIHandler; + this.roomId = roomId; this.socket = connectSocket(this.roomId); this.uaParser = new UAParser(); this.createUserAndInitSocket(); this.createPeer(); - this.setScreenSharingSourceTypeCallback = setScreenSharingSourceTypeCallback; - this.setIsDarkThemeCallback = setIsDarkThemeCallback; - this.setAppLanguageCallback = setAppLanguageCallback; - this.setDialogErrorMessageCallback = setDialogErrorMessageCallback; - this.setIsErrorDialogOpen = setIsErrorDialogOpen; - - this.video = null; - this.canvas = null; - this.largeMismatchFramesCount = 0; if (!this.roomId || this.roomId === '') { - setDialogErrorMessageCallback(ErrorMessage.UNKNOWN_ERROR); - setIsErrorDialogOpen(true); + setAndShowErrorDialogMessage(this, ErrorMessage.NOT_ALLOWED); } - setInterval(() => { - if (!this.socket.connected) { - if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) { - this.setDialogErrorMessageCallback(ErrorMessage.DENY_TO_CONNECT); - this.setIsErrorDialogOpen(true); - this.errorDialogMessage = ErrorMessage.DENY_TO_CONNECT; - } - } - }, 2000); + startSocketConnectedCheckingLoop(this); } setVideoQuality(videoQuality: VideoQuality) { @@ -162,28 +96,25 @@ export default class PeerConnection { this.videoQualityChangedCallback(); } - setErrorDialogMessage(message: ErrorMessage) { - this.errorDialogMessage = message; - } videoQualityChangedCallback() { - if (this.videoQuality !== VideoQuality.Q_AUTO) { + if (this.videoQuality === VideoQuality.Q_AUTO) { + this.peer?.send(prepareDataMessageToChangeQuality(1)); + } else { this.peer?.send( prepareDataMessageToChangeQuality( VIDEO_QUALITY_TO_DECIMAL[this.videoQuality] ) ); - } else { - this.peer?.send(prepareDataMessageToChangeQuality(1)); } } createPeer() { + // When we are testing with jest, SimplePeer() can not be created, so we just return + if (areWeTestingWithJest()) return; + const peer = new SimplePeer({ initiator: false, - // trickle: true, - // stream: null, - // allowHalfTrickle: false, config: { iceServers: [] }, sdpTransform: (sdp) => { let newSDP = sdp; @@ -196,58 +127,14 @@ export default class PeerConnection { }, }); - peer.on('stream', (stream) => { - this.videoAutoQualityOptimizer.setGoodQualityCallback(() => { - if (this.videoQuality === VideoQuality.Q_AUTO) { - this.peer?.send(prepareDataMessageToChangeQuality(1)); - } - }); - - this.videoAutoQualityOptimizer.setHalfQualityCallbak(() => { - if (this.videoQuality === VideoQuality.Q_AUTO) { - this.peer?.send(prepareDataMessageToChangeQuality(0.5)); - } - }); - - this.videoAutoQualityOptimizer.startOptimizationLoop(); - - this.setUrlCallback(stream); - setTimeout(() => { - this.peer?.send(prepareDataMessageToGetSharingSourceType()); - }, 1000); - - this.isStreamStarted = true; - }); - - peer.on('signal', (data) => { - // fired when webrtc done preparation to start call on this machine - this.sendEncryptedMessage({ - type: 'CALL_ACCEPTED', - payload: { - signalData: data, - }, - }); - }); - - peer.on('data', (data) => { - const dataJSON = JSON.parse(data); - - if (dataJSON.type === 'screen_sharing_source_type') { - this.screenSharingSourceType = dataJSON.payload.value; - if ( - this.screenSharingSourceType === 'screen' || - this.screenSharingSourceType === 'window' - ) { - this.setScreenSharingSourceTypeCallback(this.screenSharingSourceType); - } - } - }); - this.peer = peer; + peerConnectionHandlePeer(this); } initApp(user: LocalPeerUser, myIP: string) { - if (!this.socket) return; + if (!this.socket) { + throw new PeerConnectionSocketNotDefined(); + } this.socket.emit('USER_ENTER', { username: user.username, publicKey: user.publicKey, @@ -275,152 +162,36 @@ export default class PeerConnection { }); } - async sendEncryptedMessage(payload: SendEncryptedMessagePayload) { - if (!this.socket) return; - if (!this.user) return; - if (!this.partner) return; - const msg = (await prepareMessage(payload, this.user, this.partner)) as any; - this.socket.emit('ENCRYPTED_MESSAGE', msg.toSend); + sendEncryptedMessage(payload: SendEncryptedMessagePayload) { + if (!this.socket) { + throw new PeerConnectionSocketNotDefined(); + } + if (!this.user || this.user === NullUser) { + throw new PeerConnectionUserIsNotDefinedError(); + } + if (!this.partner || this.partner === NullUser) { + throw new PeerConnectionPartnerIsNotDefinedError(); + } + prepareMessage(payload, this.user, this.partner).then((msg: any) => { + this.socket.emit('ENCRYPTED_MESSAGE', msg.toSend); + }) } - async receiveEncryptedMessage(payload: ReceiveEncryptedMessagePayload) { - if (!this.user) return; - const message = (await processMessage( - payload, - this.user.privateKey - )) as any; - if (message.type === 'CALL_USER') { - this.peer?.signal(message.payload.signalData); - } - if (message.type === 'DENY_TO_CONNECT') { - if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) { - this.setDialogErrorMessageCallback(ErrorMessage.DENY_TO_CONNECT); - this.setIsErrorDialogOpen(true); - this.errorDialogMessage = ErrorMessage.DENY_TO_CONNECT; - } - } - if (message.type === 'DISCONNECT_BY_HOST_MACHINE_USER') { - if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) { - this.setDialogErrorMessageCallback(ErrorMessage.DICONNECTED); - this.setIsErrorDialogOpen(true); - this.errorDialogMessage = ErrorMessage.DICONNECTED; - } - } - if (message.type === 'ALLOWED_TO_CONNECT') { - this.hostAllowedToConnectCallback(); - } - if (message.type === 'APP_THEME') { - if (this.isDarkTheme !== message.payload.value) { - this.setIsDarkThemeCallback(message.payload.value); - this.isDarkTheme = message.payload.value; - } - } - if (message.type === 'APP_LANGUAGE') { - this.setAppLanguageCallback(message.payload.value); - } + receiveEncryptedMessage(payload: ReceiveEncryptedMessagePayload) { + peerConnectionReceiveEncryptedMessage(this, payload); } createUserAndInitSocket() { - if (!this.socket) return; + if (!this.socket) { + throw new PeerConnectionSocketNotDefined(); + } this.socket.removeAllListeners(); const userCreatedCallback = (createdUser: LocalPeerUser) => { this.user = createdUser; - this.socket.on('disconnect', () => { - // this.props.toggleSocketConnected(false); - if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) { - this.setDialogErrorMessageCallback(ErrorMessage.DICONNECTED); - this.setIsErrorDialogOpen(true); - this.errorDialogMessage = ErrorMessage.DICONNECTED; - } - }); - - this.socket.on('connect', () => { - this.socket.emit('GET_MY_IP', (ip: string) => { - this.myIP = ip; - this.uaParser.setUA(window.navigator.userAgent); - this.myOS = getOSFromUAParser(this.uaParser); - this.myDeviceType = getDeviceTypeFromUAParser(this.uaParser); - this.myBrowser = getBrowserFromUAParser(this.uaParser); - - this.initApp(createdUser, ip); - }); - }); - - this.socket.on('NOT_ALLOWED', () => { - if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) { - this.setDialogErrorMessageCallback(ErrorMessage.NOT_ALLOWED); - this.setIsErrorDialogOpen(true); - this.errorDialogMessage = ErrorMessage.NOT_ALLOWED; - } - }); - - this.socket.on('USER_ENTER', (payload: { users: PartnerPeerUser[] }) => { - const filteredPartner = payload.users.filter((v) => { - return createdUser.publicKey !== v.publicKey; - }); - - this.partner = filteredPartner[0]; - - this.sendEncryptedMessage({ - type: 'ADD_USER', - payload: { - username: createdUser.username, - publicKey: createdUser.publicKey, - isOwner: true, - id: createdUser.username, - }, - }); - - // TODO: send device details as strings here! - this.sendEncryptedMessage({ - type: 'DEVICE_DETAILS', - payload: { - socketID: this.socket.io.engine.id, - os: this.myOS, - deviceType: this.myDeviceType, - browser: this.myBrowser, - deviceScreenWidth: window.screen.width, - deviceScreenHeight: window.screen.height, - }, - }); - - this.sendEncryptedMessage({ type: 'GET_APP_THEME', payload: {} }); - this.sendEncryptedMessage({ type: 'GET_APP_LANGUAGE', payload: {} }); - - setTimeout(() => { - this.setMyDeviceDetails({ - myIP: this.myIP, - myOS: this.myOS, - myBrowser: this.myBrowser, - myDeviceType: this.myDeviceType, - }); - }, 100); - }); - - this.socket.on('USER_EXIT', (payload: any) => { - // this.props.receiveUnencryptedMessage('USER_EXIT', payload); - }); - - this.socket.on( - 'ENCRYPTED_MESSAGE', - (payload: ReceiveEncryptedMessagePayload) => { - this.receiveEncryptedMessage(payload); - } - ); - - this.socket.on('ROOM_LOCKED', (payload: any) => { - // TODO: call ROOM LOCKED callback to change react component contain ROOM LOCKED message - // @ts-ignore - // document.querySelector('#my-ip')?.innerHTML = 'ROOM LOCKED'; - if (this.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR) { - this.setDialogErrorMessageCallback(ErrorMessage.DENY_TO_CONNECT); - this.setIsErrorDialogOpen(true); - this.errorDialogMessage = ErrorMessage.UNKNOWN_ERROR; - } - }); + peerConnectionHandleSocket(this); window.addEventListener('beforeunload', (_) => { this.socket.emit('USER_DISCONNECT'); diff --git a/app/client/src/features/PeerConnection/mocks/INPUTtestWindowNavigatorUserAgent.ts b/app/client/src/features/PeerConnection/mocks/INPUTtestWindowNavigatorUserAgent.ts new file mode 100644 index 0000000..054440b --- /dev/null +++ b/app/client/src/features/PeerConnection/mocks/INPUTtestWindowNavigatorUserAgent.ts @@ -0,0 +1,2 @@ +export const INPUTtestWindowNavigatorUserAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'; diff --git a/app/client/src/features/PeerConnection/mocks/INPUTvideo500000testSdpMediaBitrate.ts b/app/client/src/features/PeerConnection/mocks/INPUTvideo500000testSdpMediaBitrate.ts new file mode 100644 index 0000000..4da0f63 --- /dev/null +++ b/app/client/src/features/PeerConnection/mocks/INPUTvideo500000testSdpMediaBitrate.ts @@ -0,0 +1,109 @@ +export const INPUTtestSdpMediaBitrate = +` +v=0 +o=- 5730467698688819135 2 IN IP4 127.0.0.1 +s=- +t=0 0 +a=group:BUNDLE 0 1 +a=msid-semantic: WMS +m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 114 115 116 +c=IN IP4 0.0.0.0 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:PY+h +a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc +a=ice-options:trickle +a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D +a=setup:active +a=mid:0 +a=extmap:1 urn:ietf:params:rtp-hdrext:toffset +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:3 urn:3gpp:video-orientation +a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type +a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing +a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space +a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid +a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id +a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id +a=recvonly +a=rtcp-mux +a=rtcp-rsize +a=rtpmap:96 VP8/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 transport-cc +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=rtpmap:97 rtx/90000 +a=fmtp:97 apt=96 +a=rtpmap:98 VP9/90000 +a=rtcp-fb:98 goog-remb +a=rtcp-fb:98 transport-cc +a=rtcp-fb:98 ccm fir +a=rtcp-fb:98 nack +a=rtcp-fb:98 nack pli +a=fmtp:98 profile-id=0 +a=rtpmap:99 rtx/90000 +a=fmtp:99 apt=98 +a=rtpmap:100 VP9/90000 +a=rtcp-fb:100 goog-remb +a=rtcp-fb:100 transport-cc +a=rtcp-fb:100 ccm fir +a=rtcp-fb:100 nack +a=rtcp-fb:100 nack pli +a=fmtp:100 profile-id=2 +a=rtpmap:101 rtx/90000 +a=fmtp:101 apt=100 +a=rtpmap:102 H264/90000 +a=rtcp-fb:102 goog-remb +a=rtcp-fb:102 transport-cc +a=rtcp-fb:102 ccm fir +a=rtcp-fb:102 nack +a=rtcp-fb:102 nack pli +a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f +a=rtpmap:121 rtx/90000 +a=fmtp:121 apt=102 +a=rtpmap:127 H264/90000 +a=rtcp-fb:127 goog-remb +a=rtcp-fb:127 transport-cc +a=rtcp-fb:127 ccm fir +a=rtcp-fb:127 nack +a=rtcp-fb:127 nack pli +a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f +a=rtpmap:120 rtx/90000 +a=fmtp:120 apt=127 +a=rtpmap:125 H264/90000 +a=rtcp-fb:125 goog-remb +a=rtcp-fb:125 transport-cc +a=rtcp-fb:125 ccm fir +a=rtcp-fb:125 nack +a=rtcp-fb:125 nack pli +a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f +a=rtpmap:107 rtx/90000 +a=fmtp:107 apt=125 +a=rtpmap:108 H264/90000 +a=rtcp-fb:108 goog-remb +a=rtcp-fb:108 transport-cc +a=rtcp-fb:108 ccm fir +a=rtcp-fb:108 nack +a=rtcp-fb:108 nack pli +a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f +a=rtpmap:109 rtx/90000 +a=fmtp:109 apt=108 +a=rtpmap:114 red/90000 +a=rtpmap:115 rtx/90000 +a=fmtp:115 apt=114 +a=rtpmap:116 ulpfec/90000 +m=application 9 UDP/DTLS/SCTP webrtc-datachannel +c=IN IP4 0.0.0.0 +b=AS:30 +a=ice-ufrag:PY+h +a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc +a=ice-options:trickle +a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D +a=setup:active +a=mid:1 +a=sctp-port:5000 +a=max-message-size:262144 +`; diff --git a/app/client/src/features/PeerConnection/mocks/OUTPUTDeviceDetailsFromUAParsed.ts b/app/client/src/features/PeerConnection/mocks/OUTPUTDeviceDetailsFromUAParsed.ts new file mode 100644 index 0000000..00081fe --- /dev/null +++ b/app/client/src/features/PeerConnection/mocks/OUTPUTDeviceDetailsFromUAParsed.ts @@ -0,0 +1,6 @@ +export const OUTPUTDeviceDetailsFromUAParsed: DeviceDetails = { + myBrowser: 'Chrome 87.0.4280.88', + myDeviceType: 'computer', + myIP: '123.123.123.123', + myOS: 'Mac OS 10.15.6', +}; diff --git a/app/client/src/features/PeerConnection/mocks/OUTPUTvideo500000testSdpMediaBitrate.ts b/app/client/src/features/PeerConnection/mocks/OUTPUTvideo500000testSdpMediaBitrate.ts new file mode 100644 index 0000000..84c6986 --- /dev/null +++ b/app/client/src/features/PeerConnection/mocks/OUTPUTvideo500000testSdpMediaBitrate.ts @@ -0,0 +1,110 @@ +export const OUTPUTtestSdpMediaBitrate = +` +v=0 +o=- 5730467698688819135 2 IN IP4 127.0.0.1 +s=- +t=0 0 +a=group:BUNDLE 0 1 +a=msid-semantic: WMS +m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 114 115 116 +c=IN IP4 0.0.0.0 +b=AS:500000 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:PY+h +a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc +a=ice-options:trickle +a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D +a=setup:active +a=mid:0 +a=extmap:1 urn:ietf:params:rtp-hdrext:toffset +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:3 urn:3gpp:video-orientation +a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type +a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing +a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space +a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid +a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id +a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id +a=recvonly +a=rtcp-mux +a=rtcp-rsize +a=rtpmap:96 VP8/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 transport-cc +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=rtpmap:97 rtx/90000 +a=fmtp:97 apt=96 +a=rtpmap:98 VP9/90000 +a=rtcp-fb:98 goog-remb +a=rtcp-fb:98 transport-cc +a=rtcp-fb:98 ccm fir +a=rtcp-fb:98 nack +a=rtcp-fb:98 nack pli +a=fmtp:98 profile-id=0 +a=rtpmap:99 rtx/90000 +a=fmtp:99 apt=98 +a=rtpmap:100 VP9/90000 +a=rtcp-fb:100 goog-remb +a=rtcp-fb:100 transport-cc +a=rtcp-fb:100 ccm fir +a=rtcp-fb:100 nack +a=rtcp-fb:100 nack pli +a=fmtp:100 profile-id=2 +a=rtpmap:101 rtx/90000 +a=fmtp:101 apt=100 +a=rtpmap:102 H264/90000 +a=rtcp-fb:102 goog-remb +a=rtcp-fb:102 transport-cc +a=rtcp-fb:102 ccm fir +a=rtcp-fb:102 nack +a=rtcp-fb:102 nack pli +a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f +a=rtpmap:121 rtx/90000 +a=fmtp:121 apt=102 +a=rtpmap:127 H264/90000 +a=rtcp-fb:127 goog-remb +a=rtcp-fb:127 transport-cc +a=rtcp-fb:127 ccm fir +a=rtcp-fb:127 nack +a=rtcp-fb:127 nack pli +a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f +a=rtpmap:120 rtx/90000 +a=fmtp:120 apt=127 +a=rtpmap:125 H264/90000 +a=rtcp-fb:125 goog-remb +a=rtcp-fb:125 transport-cc +a=rtcp-fb:125 ccm fir +a=rtcp-fb:125 nack +a=rtcp-fb:125 nack pli +a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f +a=rtpmap:107 rtx/90000 +a=fmtp:107 apt=125 +a=rtpmap:108 H264/90000 +a=rtcp-fb:108 goog-remb +a=rtcp-fb:108 transport-cc +a=rtcp-fb:108 ccm fir +a=rtcp-fb:108 nack +a=rtcp-fb:108 nack pli +a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f +a=rtpmap:109 rtx/90000 +a=fmtp:109 apt=108 +a=rtpmap:114 red/90000 +a=rtpmap:115 rtx/90000 +a=fmtp:115 apt=114 +a=rtpmap:116 ulpfec/90000 +m=application 9 UDP/DTLS/SCTP webrtc-datachannel +c=IN IP4 0.0.0.0 +b=AS:30 +a=ice-ufrag:PY+h +a=ice-pwd:eYoy9PHXsilgXAbK7MSIMUJc +a=ice-options:trickle +a=fingerprint:sha-256 73:1D:63:11:3E:2F:A4:AA:ED:37:4B:D6:0F:A2:60:7A:A3:9B:EC:D9:D1:AF:C3:E0:53:59:4A:E1:D5:A9:EF:2D +a=setup:active +a=mid:1 +a=sctp-port:5000 +a=max-message-size:262144 +`; diff --git a/app/client/src/features/PeerConnection/peerConnectionHandlePeer.spec.ts b/app/client/src/features/PeerConnection/peerConnectionHandlePeer.spec.ts new file mode 100644 index 0000000..8bf9c11 --- /dev/null +++ b/app/client/src/features/PeerConnection/peerConnectionHandlePeer.spec.ts @@ -0,0 +1,300 @@ +import SimplePeer from 'simple-peer'; +import PeerConnection from '.'; +import Crypto from '../../utils/crypto'; +import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer'; +import { VideoQuality } from '../VideoAutoQualityOptimizer/VideoQualityEnum'; +import peerConnectionHandlePeer, { + getSharingShourceType, +} from './peerConnectionHandlePeer'; +import PeerConnectionPeerIsNullError from './errors/PeerConnectionPeerIsNullError'; +import PeerConnectionUIHandler from './PeerConnectionUIHandler'; +import { + prepareDataMessageToChangeQuality, + prepareDataMessageToGetSharingSourceType, +} from './simplePeerDataMessages'; + +jest.useFakeTimers(); + +jest.mock('../../utils/crypto.ts'); +jest.mock('simple-peer', () => { + return jest.fn().mockImplementation(() => { + const listeners = new Map(); + return { + ...jest.requireActual('simple-peer'), + on: jest.fn().mockImplementation((s: string, callback: any) => { + if (!listeners.has(s)) { + listeners.set(s, []); + } + listeners.get(s)?.push(callback); + }), + emit: jest.fn().mockImplementation((s: string, any: any) => { + listeners.forEach((callbacks, key) => { + callbacks.forEach((callback) => { + if (key === s) { + callback(any); + } + }); + }); + }), + send: jest.fn().mockImplementation((_: string) => {}), + }; + }); +}); + +const screen_sharing_source_type_JSON_DATA = + ' \ + { \ + "type": "screen_sharing_source_type", \ + "payload": { \ + "value": "screen" \ + } \ + } \ +'; + +const window_sharing_source_type_JSON_DATA = + ' \ + { \ + "type": "screen_sharing_source_type", \ + "payload": { \ + "value": "window" \ + } \ + } \ +'; + +describe('peerConnectionHandlePeer callback', () => { + let peerConnection: PeerConnection; + let videoQualityOptimizer: VideoAutoQualityOptimizer; + + const setURLCallbackMock = jest.fn(); + + beforeEach(() => { + videoQualityOptimizer = new VideoAutoQualityOptimizer(); + jest.spyOn(videoQualityOptimizer, 'startOptimizationLoop'); + jest.spyOn(videoQualityOptimizer, 'goodQualityCallback'); + jest.spyOn(videoQualityOptimizer, 'halfQualityCallbak'); + peerConnection = new PeerConnection( + '123', + setURLCallbackMock, + new Crypto(), + videoQualityOptimizer, + new PeerConnectionUIHandler( + true, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn() + ) + ); + jest.spyOn(peerConnection, 'sendEncryptedMessage'); + peerConnection.peer = new SimplePeer(); + + peerConnection.user = { + username: 'af', + privateKey: 'af', + publicKey: 'af', + }; + peerConnection.partner = { + username: 'af', + publicKey: 'af', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('when peerConnection with peer null passed as parameter', () => { + function callPeerConnectionHandlePeerWithPeerNull() { + peerConnection.peer = null; + + peerConnectionHandlePeer(peerConnection); + } + it('should throw an error', () => { + peerConnection.peer = null; + + expect(callPeerConnectionHandlePeerWithPeerNull).toThrow( + new PeerConnectionPeerIsNullError() + ); + }); + }); + + describe('when peerConnectionHandlePeer() is called properly', () => { + it('should have set .on("stream", () => {}) callback on PeerConnection.peer', () => { + jest.requireMock('simple-peer'); + + peerConnectionHandlePeer(peerConnection); + + expect(peerConnection.peer?.on).toBeCalledWith( + 'stream', + expect.anything() + ); + }); + + it('should have set .on("data", () => {}) callback on PeerConnection.peer', () => { + jest.requireMock('simple-peer'); + + peerConnectionHandlePeer(peerConnection); + + expect(peerConnection.peer?.on).toBeCalledWith('data', expect.anything()); + }); + + it('should have set .on("signal", () => {}) callback on PeerConnection.peer', () => { + jest.requireMock('simple-peer'); + + peerConnectionHandlePeer(peerConnection); + + expect(peerConnection.peer?.on).toBeCalledWith( + 'signal', + expect.anything() + ); + }); + + describe('when "stream" event occured', () => { + it('should start video quality optimization loop', () => { + peerConnectionHandlePeer(peerConnection); + + peerConnection.peer?.emit('stream'); + + expect( + peerConnection.videoAutoQualityOptimizer.startOptimizationLoop + ).toBeCalled(); + }); + + it('should call getSharingShourceType function to get sharing source type from host', () => { + peerConnectionHandlePeer(peerConnection); + + peerConnection.peer?.emit('stream'); + + expect(setTimeout).toHaveBeenLastCalledWith( + getSharingShourceType, + 1000, + peerConnection + ); + }); + + describe('when quality is AUTO and when video quality optimizer requiests GOOD quality', () => { + it('should call .send with proper data message', () => { + peerConnectionHandlePeer(peerConnection); + peerConnection.peer?.emit('stream'); + + peerConnection.videoAutoQualityOptimizer.goodQualityCallback(); + + expect(peerConnection.videoQuality).toBe(VideoQuality.Q_AUTO); + expect(peerConnection.peer?.send).toBeCalledWith( + prepareDataMessageToChangeQuality(1) + ); + }); + }); + + describe('when quality is NOT AUTO and when video quality optimizer requiests GOOD quality', () => { + it('should call NOT .send with proper data message', () => { + peerConnection.videoQuality = VideoQuality.Q_25_PERCENT; + peerConnectionHandlePeer(peerConnection); + peerConnection.peer?.emit('stream'); + + peerConnection.videoAutoQualityOptimizer.goodQualityCallback(); + + expect(peerConnection.peer?.send).not.toBeCalled(); + }); + }); + + describe('when quality is AUTO and when video quality optimizer requiests HALF quality', () => { + it('should call .send with proper data message', () => { + peerConnectionHandlePeer(peerConnection); + peerConnection.peer?.emit('stream'); + + peerConnection.videoAutoQualityOptimizer.halfQualityCallbak(); + + expect(peerConnection.videoQuality).toBe(VideoQuality.Q_AUTO); + expect(peerConnection.peer?.send).toBeCalledWith( + prepareDataMessageToChangeQuality(0.5) + ); + }); + }); + }); + + describe('when quality is NOT AUTO and when video quality optimizer requiests GOOD quality', () => { + it('should call NOT .send with proper data message', () => { + peerConnection.videoQuality = VideoQuality.Q_25_PERCENT; + peerConnectionHandlePeer(peerConnection); + peerConnection.peer?.emit('stream'); + + peerConnection.videoAutoQualityOptimizer.halfQualityCallbak(); + + expect(peerConnection.peer?.send).not.toBeCalled(); + }); + }); + + describe('when "signal" event occured', () => { + it('should call sendEncryptedMessage on peer connection', () => { + peerConnectionHandlePeer(peerConnection); + peerConnection.peer?.emit('signal'); + + expect(peerConnection.sendEncryptedMessage).toBeCalled(); + }); + }); + + describe('when "data" event occured', () => { + describe('when data.type is "screen_sharing_source_type"', () => { + describe('when dataJSON.payload.value === screen', () => { + it('should call UIHandler.setScreenSharingSourceTypeCallback with "screen" string as parameter', () => { + peerConnectionHandlePeer(peerConnection); + + peerConnection.peer?.emit( + 'data', + screen_sharing_source_type_JSON_DATA + ); + + expect( + peerConnection.UIHandler.setScreenSharingSourceTypeCallback + ).toBeCalledWith('screen'); + }); + }); + + describe('when dataJSON.payload.value === window', () => { + it('should call UIHandler.setScreenSharingSourceTypeCallback with "window" string as parameter', () => { + peerConnectionHandlePeer(peerConnection); + + peerConnection.peer?.emit( + 'data', + window_sharing_source_type_JSON_DATA + ); + + expect( + peerConnection.UIHandler.setScreenSharingSourceTypeCallback + ).toBeCalledWith('window'); + }); + }); + + describe('when dataJSON.payload.value is NOT "screen" or "window"', () => { + it('should do nothing', () => { + expect( + peerConnection.UIHandler.setScreenSharingSourceTypeCallback + ).not.toBeCalled(); + }); + }); + }); + + describe('when data.type is NOT "screen_sharing_source_type"', () => { + it('should do nothing', () => { + expect( + peerConnection.UIHandler.setScreenSharingSourceTypeCallback + ).not.toBeCalled(); + }); + }); + }); + }); + + describe('when getSharingSourceType is called properly', () => { + it('should call .send method with proper payload to get sharing source type from host', () => { + getSharingShourceType(peerConnection); + + expect(peerConnection.peer?.send).toBeCalledWith( + prepareDataMessageToGetSharingSourceType() + ); + }); + }); +}); diff --git a/app/client/src/features/PeerConnection/peerConnectionHandlePeer.ts b/app/client/src/features/PeerConnection/peerConnectionHandlePeer.ts new file mode 100644 index 0000000..69d21b6 --- /dev/null +++ b/app/client/src/features/PeerConnection/peerConnectionHandlePeer.ts @@ -0,0 +1,76 @@ +import PeerConnection from '.'; +import { + prepareDataMessageToChangeQuality, + prepareDataMessageToGetSharingSourceType, +} from './simplePeerDataMessages'; +import { VideoQuality } from '../VideoAutoQualityOptimizer/VideoQualityEnum'; +import PeerConnectionPeerIsNullError from './errors/PeerConnectionPeerIsNullError'; + +export function getSharingShourceType(peerConnection: PeerConnection) { + try { + peerConnection.peer?.send(prepareDataMessageToGetSharingSourceType()); + } catch (e) { + console.log(e); + } +} + +export default (peerConnection: PeerConnection) => { + if (peerConnection.peer === null) { + throw new PeerConnectionPeerIsNullError(); + } + peerConnection.peer.on('stream', (stream) => { + peerConnection.setUrlCallback(stream); + + peerConnection.videoAutoQualityOptimizer.setGoodQualityCallback(() => { + if (peerConnection.videoQuality === VideoQuality.Q_AUTO) { + try { + peerConnection.peer?.send(prepareDataMessageToChangeQuality(1)); + } catch (e) { + console.log(e); + } + } + }); + + peerConnection.videoAutoQualityOptimizer.setHalfQualityCallbak(() => { + if (peerConnection.videoQuality === VideoQuality.Q_AUTO) { + try { + peerConnection.peer?.send(prepareDataMessageToChangeQuality(0.5)); + } catch (e) { + console.log(e); + } + } + }); + + peerConnection.videoAutoQualityOptimizer.startOptimizationLoop(); + + setTimeout(getSharingShourceType, 1000, peerConnection); + + peerConnection.isStreamStarted = true; + }); + + peerConnection.peer.on('signal', (data) => { + // fired when webrtc done preparation to start call on peerConnection machine + peerConnection.sendEncryptedMessage({ + type: 'CALL_ACCEPTED', + payload: { + signalData: data, + }, + }); + }); + + peerConnection.peer.on('data', (data) => { + const dataJSON = JSON.parse(data); + + if (dataJSON.type === 'screen_sharing_source_type') { + peerConnection.screenSharingSourceType = dataJSON.payload.value; + if ( + peerConnection.screenSharingSourceType === 'screen' || + peerConnection.screenSharingSourceType === 'window' + ) { + peerConnection.UIHandler.setScreenSharingSourceTypeCallback( + peerConnection.screenSharingSourceType + ); + } + } + }); +}; diff --git a/app/client/src/features/PeerConnection/peerConnectionHandleSocket.spec.ts b/app/client/src/features/PeerConnection/peerConnectionHandleSocket.spec.ts new file mode 100644 index 0000000..3d21d89 --- /dev/null +++ b/app/client/src/features/PeerConnection/peerConnectionHandleSocket.spec.ts @@ -0,0 +1,241 @@ +import { OUTPUTDeviceDetailsFromUAParsed } from './mocks/OUTPUTDeviceDetailsFromUAParsed'; +import { INPUTtestWindowNavigatorUserAgent } from './mocks/INPUTtestWindowNavigatorUserAgent'; +jest.mock('./setAndShowErrorDialogMessage'); +// import SimplePeer from 'simple-peer'; +import PeerConnection from '.'; +// import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum'; +import Crypto from '../../utils/crypto'; +import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer'; +import PeerConnectionUIHandler from './PeerConnectionUIHandler'; +import peerConnectionHandleSocket, { + getMyIPCallback, +} from './peerConnectionHandleSocket'; +import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage'; +import PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined'; + +jest.useFakeTimers(); + +// jest.mock('.'); +jest.mock('../../utils/crypto.ts'); + +const TEST_IP = '123.123.123.123'; + +describe('peerConnectionHandleSocket callback', () => { + let peerConnection: PeerConnection; + + beforeEach(() => { + peerConnection = new PeerConnection( + '123', + jest.fn(), + new Crypto(), + new VideoAutoQualityOptimizer(), + new PeerConnectionUIHandler( + true, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn() + ) + ); + + peerConnection.initApp = jest.fn(); + + const listeners = new Map(); + + peerConnection.socket = { + on: jest.fn().mockImplementation((s: string, callback: any) => { + if (!listeners.has(s)) { + listeners.set(s, []); + } + listeners.get(s)?.push(callback); + }), + emit: jest.fn().mockImplementation((s: string, any: any) => { + listeners.forEach((callbacks, key) => { + callbacks.forEach((callback) => { + if (key === s) { + callback(any); + } + }); + }); + }), + io: { + engine: { + id: '434241' + } + } + }; + + peerConnection.receiveEncryptedMessage = jest.fn(); + peerConnection.sendEncryptedMessage = jest.fn(); + + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe('when peerConnectionHandleSocket is called with .socket undefined', () => { + it('should throw an error', () => { + peerConnection.socket = undefined; + + try { + peerConnectionHandleSocket(peerConnection); + fail('PeerConnectionSocketNotDefined should be thrown here'); + } catch (e) { + expect(e).toEqual(new PeerConnectionSocketNotDefined()); + } + }); + }); + + describe('when peerConnectionHandleSocket is called properly', () => { + describe('when socket.on("disconnect") occured', () => { + it('should call setAndShowErrorDialogMessage to show user that they are disconnected', () => { + peerConnectionHandleSocket(peerConnection); + + peerConnection.socket.emit('disconnect'); + + expect(setAndShowErrorDialogMessage).toBeCalled(); + }); + }); + + describe('when socket.on("connect") occured', () => { + it('should call socket.emit("GET_MY_IP")', () => { + peerConnectionHandleSocket(peerConnection); + + peerConnection.socket.emit('connect'); + + expect(peerConnection.socket.emit).toBeCalledWith( + 'GET_MY_IP', + expect.anything() + ); + }); + }); + + describe('when socket.on("ENCRYPTED_MESSAGE") occured', () => { + it('should call peerConnection receiveEncryptedMessage', () => { + peerConnectionHandleSocket(peerConnection); + + peerConnection.socket.emit('ENCRYPTED_MESSAGE'); + + expect(peerConnection.receiveEncryptedMessage).toBeCalled(); + }); + }); + + describe('when socket.on("NOT_ALLOWED") occured', () => { + it('should change UI accordingly', () => { + peerConnectionHandleSocket(peerConnection); + + peerConnection.socket.emit('NOT_ALLOWED'); + + expect(setAndShowErrorDialogMessage).toBeCalled(); + }); + }); + + describe('when socket.on("ROOM_LOCKED") occured', () => { + it('should change UI accordingly', () => { + peerConnectionHandleSocket(peerConnection); + + peerConnection.socket.emit('ROOM_LOCKED'); + + expect(setAndShowErrorDialogMessage).toBeCalled(); + }); + }); + + describe('when socket.on("USER_ENTER") occured', () => { + it('should call setMyDeviceDetails on UIHandler', () => { + peerConnectionHandleSocket(peerConnection); + + peerConnection.socket.emit('USER_ENTER', { + users: [{ username: 'asdf', publicKey: '1234' }], + }); + + jest.advanceTimersByTime(1000); + + expect(peerConnection.UIHandler.setMyDeviceDetails).toBeCalled(); + }); + + it('should call sendEncryptedMessage with ADD_USER type', () => { + peerConnectionHandleSocket(peerConnection); + + peerConnection.socket.emit('USER_ENTER', { + users: [{ username: 'asdf', publicKey: '1234' }], + }); + + expect(peerConnection.sendEncryptedMessage).toBeCalledWith({ + type: 'ADD_USER', + payload: expect.anything(), + }); + }); + + it('should call sendEncryptedMessage with DEVICE_DETAILS type', () => { + peerConnectionHandleSocket(peerConnection); + + peerConnection.socket.emit('USER_ENTER', { + users: [{ username: 'asdf', publicKey: '1234' }], + }); + + expect(peerConnection.sendEncryptedMessage).toBeCalledWith({ + type: 'DEVICE_DETAILS', + payload: expect.anything(), + }); + }); + + it('should call sendEncryptedMessage with GET_APP_THEME type', () => { + peerConnectionHandleSocket(peerConnection); + + peerConnection.socket.emit('USER_ENTER', { + users: [{ username: 'asdf', publicKey: '1234' }], + }); + + expect(peerConnection.sendEncryptedMessage).toBeCalledWith({ + type: 'GET_APP_THEME', + payload: expect.anything(), + }); + }); + + it('should call sendEncryptedMessage with GET_APP_LANGUAGE type', () => { + peerConnectionHandleSocket(peerConnection); + + peerConnection.socket.emit('USER_ENTER', { + users: [{ username: 'asdf', publicKey: '1234' }], + }); + + expect(peerConnection.sendEncryptedMessage).toBeCalledWith({ + type: 'GET_APP_LANGUAGE', + payload: expect.anything(), + }); + }); + + }); + }); + + describe('when getMyIPCallback is called properly', () => { + it('should call initApp on peerConnection', () => { + getMyIPCallback( + peerConnection, + TEST_IP, + INPUTtestWindowNavigatorUserAgent + ); + + expect(peerConnection.initApp).toBeCalled(); + }); + + it('should make peerConnection.myDeviceDetails with proper parameters', () => { + getMyIPCallback( + peerConnection, + TEST_IP, + INPUTtestWindowNavigatorUserAgent + ); + + expect(peerConnection.myDeviceDetails).toEqual( + OUTPUTDeviceDetailsFromUAParsed + ); + }); + }); +}); diff --git a/app/client/src/features/PeerConnection/peerConnectionHandleSocket.ts b/app/client/src/features/PeerConnection/peerConnectionHandleSocket.ts new file mode 100644 index 0000000..721902e --- /dev/null +++ b/app/client/src/features/PeerConnection/peerConnectionHandleSocket.ts @@ -0,0 +1,118 @@ +import PeerConnection from '.'; +import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum'; +import { + getBrowserFromUAParser, + getDeviceTypeFromUAParser, + getOSFromUAParser, +} from '../../utils/userAgentParserHelpers'; +import PeerConnectionSocketNotDefined from './errors/PeerConnectionSocketNotDefined'; +import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage'; + +export function getMyIPCallback( + peerConnection: PeerConnection, + ip: string, + userAgent: string +) { + peerConnection.myDeviceDetails.myIP = ip; + + peerConnection.uaParser.setUA(userAgent); + peerConnection.myDeviceDetails.myOS = getOSFromUAParser( + peerConnection.uaParser + ); + peerConnection.myDeviceDetails.myDeviceType = getDeviceTypeFromUAParser( + peerConnection.uaParser + ); + peerConnection.myDeviceDetails.myBrowser = getBrowserFromUAParser( + peerConnection.uaParser + ); + + peerConnection.initApp(peerConnection.user, ip); +} + +export default (peerConnection: PeerConnection) => { + if (!peerConnection.socket) { + throw new PeerConnectionSocketNotDefined(); + } + + peerConnection.socket.on('disconnect', () => { + setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED); + }); + + peerConnection.socket.on('connect', () => { + peerConnection.socket.emit('GET_MY_IP', (ip: string) => { + getMyIPCallback(peerConnection, ip, window.navigator.userAgent); + }); + }); + + peerConnection.socket.on('NOT_ALLOWED', () => { + setAndShowErrorDialogMessage(peerConnection, ErrorMessage.NOT_ALLOWED); + }); + + peerConnection.socket.on( + 'USER_ENTER', + (payload: { users: PartnerPeerUser[] }) => { + const filteredPartner = payload.users.filter((v) => { + return peerConnection.user.publicKey !== v.publicKey; + }); + + peerConnection.partner = filteredPartner[0]; + + if (!peerConnection.partner) return; + + peerConnection.sendEncryptedMessage({ + type: 'ADD_USER', + payload: { + username: peerConnection.user.username, + publicKey: peerConnection.user.publicKey, + isOwner: true, + id: peerConnection.user.username, + }, + }); + + peerConnection.sendEncryptedMessage({ + type: 'DEVICE_DETAILS', + payload: { + socketID: peerConnection.socket.io.engine.id, + os: peerConnection.myDeviceDetails.myOS, + deviceType: peerConnection.myDeviceDetails.myDeviceType, + browser: peerConnection.myDeviceDetails.myBrowser, + deviceScreenWidth: window.screen.width, + deviceScreenHeight: window.screen.height, + }, + }); + + peerConnection.sendEncryptedMessage({ + type: 'GET_APP_THEME', + payload: {}, + }); + peerConnection.sendEncryptedMessage({ + type: 'GET_APP_LANGUAGE', + payload: {}, + }); + + setTimeout(() => { + peerConnection.UIHandler.setMyDeviceDetails({ + myIP: peerConnection.myDeviceDetails.myIP, + myOS: peerConnection.myDeviceDetails.myOS, + myBrowser: peerConnection.myDeviceDetails.myBrowser, + myDeviceType: peerConnection.myDeviceDetails.myDeviceType, + }); + }, 100); + } + ); + + // peerConnection.socket.on('USER_EXIT', (payload: any) => { + // // peerConnection.props.receiveUnencryptedMessage('USER_EXIT', payload); + // }); + + peerConnection.socket.on( + 'ENCRYPTED_MESSAGE', + (payload: ReceiveEncryptedMessagePayload) => { + peerConnection.receiveEncryptedMessage(payload); + } + ); + + peerConnection.socket.on('ROOM_LOCKED', () => { + setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT); + }); +}; diff --git a/app/client/src/features/PeerConnection/peerConnectionReceiveEncryptedMessage.spec.ts b/app/client/src/features/PeerConnection/peerConnectionReceiveEncryptedMessage.spec.ts new file mode 100644 index 0000000..5a10877 --- /dev/null +++ b/app/client/src/features/PeerConnection/peerConnectionReceiveEncryptedMessage.spec.ts @@ -0,0 +1,284 @@ +jest.mock('../../utils/message'); +jest.mock('./setAndShowErrorDialogMessage'); + +import PeerConnection from '.'; +import Crypto from '../../utils/crypto'; +import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer'; +import PeerConnectionUIHandler from './PeerConnectionUIHandler'; +import peerConnectionReceiveEncryptedMessage from './peerConnectionReceiveEncryptedMessage'; +import NullUser from './NullUser'; +import PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError'; +import SimplePeer from 'simple-peer'; +import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage'; +import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum'; + +jest.useFakeTimers(); + +const TEST_USER = { username: 'asdf', publicKey: 'ff', privateKey: 'sss' }; + +const DUMMY_PAYLOAD = { + payload: '', + signature: '', + iv: '', + keys: [], +}; + +const CALL_USER_PAYLOAD = { + payload: 'CALL_USER', + signature: '', + iv: '', + keys: [], +}; + +const DENY_TO_CONNECT_PAYLOAD = { + payload: 'DENY_TO_CONNECT', + signature: '', + iv: '', + keys: [], +}; + +const DISCONNECT_BY_HOST_MACHINE_USER = { + payload: 'DISCONNECT_BY_HOST_MACHINE_USER', + signature: '', + iv: '', + keys: [], +}; + +const ALLOWED_TO_CONNECT_PAYLOAD = { + payload: 'ALLOWED_TO_CONNECT', + signature: '', + iv: '', + keys: [], +}; + +const APP_THEME_PAYLOAD = { + payload: 'APP_THEME', + signature: '', + iv: '', + keys: [], +}; + +const APP_LANGUAGE_PAYLOAD = { + payload: 'APP_LANGUAGE', + signature: '', + iv: '', + keys: [], +}; + +jest.mock('../../utils/message', () => { + return { + process: (payload: any) => { + return new Promise((resolve) => { + if (payload.payload === 'CALL_USER') { + resolve({ + type: 'CALL_USER', + payload: { + signalData: '1signal', + }, + }); + } + if (payload.payload === 'DENY_TO_CONNECT') { + resolve({ + type: 'DENY_TO_CONNECT', + payload: {}, + }); + } + if (payload.payload === 'DISCONNECT_BY_HOST_MACHINE_USER') { + resolve({ + type: 'DISCONNECT_BY_HOST_MACHINE_USER', + payload: {}, + }); + } + if (payload.payload === 'ALLOWED_TO_CONNECT') { + resolve({ + type: 'ALLOWED_TO_CONNECT', + payload: {}, + }); + } + if (payload.payload === 'APP_THEME') { + resolve({ + type: 'APP_THEME', + payload: { + value: false, + }, + }); + } + if (payload.payload === 'APP_LANGUAGE') { + resolve({ + type: 'APP_LANGUAGE', + payload: { + value: 'latin', + }, + }); + } + resolve(); + }); + }, + }; +}); + +jest.mock('simple-peer', () => { + return jest.fn().mockImplementation(() => { + const listeners = new Map(); + return { + ...jest.requireActual('simple-peer'), + on: jest.fn().mockImplementation((s: string, callback: any) => { + if (!listeners.has(s)) { + listeners.set(s, []); + } + listeners.get(s)?.push(callback); + }), + emit: jest.fn().mockImplementation((s: string, any: any) => { + listeners.forEach((callbacks, key) => { + callbacks.forEach((callback) => { + if (key === s) { + callback(any); + } + }); + }); + }), + send: jest.fn().mockImplementation((_: string) => {}), + signal: jest.fn().mockImplementation((_: string) => {}), + }; + }); +}); + +describe('peerConnectionReceiveEncryptedMessage', () => { + let peerConnection: PeerConnection; + + beforeEach(() => { + jest.clearAllMocks(); + + peerConnection = new PeerConnection( + '123', + jest.fn(), + new Crypto(), + new VideoAutoQualityOptimizer(), + new PeerConnectionUIHandler( + true, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn() + ) + ); + peerConnection.user = TEST_USER; + peerConnection.peer = new SimplePeer(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + describe('when peerConnectionReceiveEncryptedMessage is called', () => { + describe('when peerConnection.user is NullUser', () => { + it('should throw and error', () => { + peerConnection.user = NullUser; + + peerConnectionReceiveEncryptedMessage( + peerConnection, + DUMMY_PAYLOAD + ).catch((e) => expect(e).toEqual(new PeerConnectionUserIsNotDefinedError())); + }); + }); + + describe('when processedMessageType is "CALL_USER"', () => { + it('should call .signal on simple-peer', async () => { + await peerConnectionReceiveEncryptedMessage( + peerConnection, + CALL_USER_PAYLOAD + ); + + expect(peerConnection.peer?.signal).toBeCalledWith('1signal'); + }); + }); + + describe('when processedMessageType is "DENY_TO_CONNECT"', () => { + it('should call setAndShowErrorDialogMessage with DENY_TO_CONNECT error', async () => { + await peerConnectionReceiveEncryptedMessage( + peerConnection, + DENY_TO_CONNECT_PAYLOAD + ); + + expect(setAndShowErrorDialogMessage).toBeCalledWith( + peerConnection, + ErrorMessage.DENY_TO_CONNECT + ); + }); + }); + + describe('when processedMessageType is "DISCONNECT_BY_HOST_MACHINE_USER"', () => { + it('should call setAndShowErrorDialogMessage with DISCONNECT_BY_HOST_MACHINE_USER error', async () => { + await peerConnectionReceiveEncryptedMessage( + peerConnection, + DISCONNECT_BY_HOST_MACHINE_USER + ); + + expect(setAndShowErrorDialogMessage).toBeCalledWith( + peerConnection, + ErrorMessage.DISCONNECTED + ); + }); + }); + + describe('when processedMessageType is "ALLOWED_TO_CONNECT"', () => { + it('should call setAndShowErrorDialogMessage with ALLOWED_TO_CONNECT error', async () => { + await peerConnectionReceiveEncryptedMessage( + peerConnection, + ALLOWED_TO_CONNECT_PAYLOAD + ); + + expect( + peerConnection.UIHandler.hostAllowedToConnectCallback + ).toBeCalled(); + }); + }); + + describe('when processedMessageType is "APP_THEME"', () => { + it('should call setAndShowErrorDialogMessage with APP_THEME error', async () => { + await peerConnectionReceiveEncryptedMessage( + peerConnection, + APP_THEME_PAYLOAD + ); + + expect(peerConnection.UIHandler.setIsDarkThemeCallback).toBeCalledWith( + false + ); + expect(peerConnection.UIHandler.isDarkTheme).toBe(false); + }); + }); + + describe('when processedMessageType is "APP_THEME" and current theme is the same as received', () => { + it('should call setAndShowErrorDialogMessage with APP_LANGUAGE error', async () => { + peerConnection.UIHandler.isDarkTheme = false; + + await peerConnectionReceiveEncryptedMessage( + peerConnection, + APP_LANGUAGE_PAYLOAD + ); + + expect(peerConnection.UIHandler.setIsDarkThemeCallback).not.toBeCalledWith( + false + ); + expect(peerConnection.UIHandler.isDarkTheme).toBe(false); + }); + }); + + describe('when processedMessageType is "APP_LANGUAGE"', () => { + it('should call setAndShowErrorDialogMessage with APP_LANGUAGE error', async () => { + await peerConnectionReceiveEncryptedMessage( + peerConnection, + APP_LANGUAGE_PAYLOAD + ); + + expect(peerConnection.UIHandler.setAppLanguageCallback).toBeCalledWith( + 'latin' + ); + }); + }); + }); +}); diff --git a/app/client/src/features/PeerConnection/peerConnectionReceiveEncryptedMessage.ts b/app/client/src/features/PeerConnection/peerConnectionReceiveEncryptedMessage.ts new file mode 100644 index 0000000..9701e10 --- /dev/null +++ b/app/client/src/features/PeerConnection/peerConnectionReceiveEncryptedMessage.ts @@ -0,0 +1,40 @@ +import PeerConnection from '.'; +import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum'; +import { process as processMessage } from '../../utils/message'; +import NullUser from './NullUser'; +import PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError'; +import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage'; + +export default async ( + peerConnection: PeerConnection, + payload: ReceiveEncryptedMessagePayload +) => { + if (peerConnection.user === NullUser) { + throw new PeerConnectionUserIsNotDefinedError(); + } + const message = (await processMessage( + payload, + peerConnection.user.privateKey + )) as any; + if (message.type === 'CALL_USER') { + peerConnection.peer?.signal(message.payload.signalData); + } + if (message.type === 'DENY_TO_CONNECT') { + setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT); + } + if (message.type === 'DISCONNECT_BY_HOST_MACHINE_USER') { + setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED); + } + if (message.type === 'ALLOWED_TO_CONNECT') { + peerConnection.UIHandler.hostAllowedToConnectCallback(); + } + if (message.type === 'APP_THEME') { + if (peerConnection.UIHandler.isDarkTheme !== message.payload.value) { + peerConnection.UIHandler.setIsDarkThemeCallback(message.payload.value); + peerConnection.UIHandler.isDarkTheme = message.payload.value; + } + } + if (message.type === 'APP_LANGUAGE') { + peerConnection.UIHandler.setAppLanguageCallback(message.payload.value); + } +}; diff --git a/app/client/src/features/PeerConnection/prepareDataMessageToChangeQuality.ts b/app/client/src/features/PeerConnection/prepareDataMessageToChangeQuality.ts deleted file mode 100644 index a19cfb5..0000000 --- a/app/client/src/features/PeerConnection/prepareDataMessageToChangeQuality.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default (q: number) => { - return ` - { - "type": "set_video_quality", - "payload": { - "value": ${q} - } - } - `; -}; diff --git a/app/client/src/features/PeerConnection/prepareDataMessageToGetSharingSourceType.ts b/app/client/src/features/PeerConnection/prepareDataMessageToGetSharingSourceType.ts deleted file mode 100644 index 5306978..0000000 --- a/app/client/src/features/PeerConnection/prepareDataMessageToGetSharingSourceType.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default () => { - return ` - { - "type": "get_sharing_source_type", - "payload": { - } - } - `; -}; diff --git a/app/client/src/features/PeerConnection/setAndShowErrorDialogMessage.spec.ts b/app/client/src/features/PeerConnection/setAndShowErrorDialogMessage.spec.ts new file mode 100644 index 0000000..1aeda37 --- /dev/null +++ b/app/client/src/features/PeerConnection/setAndShowErrorDialogMessage.spec.ts @@ -0,0 +1,112 @@ +import SimplePeer from 'simple-peer'; +import PeerConnection from '.'; +import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum'; +import Crypto from '../../utils/crypto'; +import VideoAutoQualityOptimizer from '../VideoAutoQualityOptimizer'; +import PeerConnectionUIHandler from './PeerConnectionUIHandler'; +import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage'; + +jest.useFakeTimers(); + +jest.mock('../../utils/crypto.ts'); +jest.mock('simple-peer', () => { + return jest.fn().mockImplementation(() => { + const listeners = new Map(); + return { + ...jest.requireActual('simple-peer'), + on: jest.fn().mockImplementation((s: string, callback: any) => { + if (!listeners.has(s)) { + listeners.set(s, []); + } + listeners.get(s)?.push(callback); + }), + emit: jest.fn().mockImplementation((s: string, any: any) => { + listeners.forEach((callbacks, key) => { + callbacks.forEach((callback) => { + if (key === s) { + callback(any); + } + }); + }); + }), + send: jest.fn().mockImplementation((_: string) => {}), + }; + }); +}); + +describe('peerConnectionHandlePeer callback', () => { + let peerConnection: PeerConnection; + + const setIsErrorDialogOpen = jest.fn(); + + beforeEach(() => { + peerConnection = new PeerConnection( + '123', + jest.fn(), + new Crypto(), + new VideoAutoQualityOptimizer(), + new PeerConnectionUIHandler( + true, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + setIsErrorDialogOpen + ) + ); + peerConnection.peer = new SimplePeer(); + peerConnection.UIHandler.errorDialogMessage = ErrorMessage.UNKNOWN_ERROR; + + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe('when setAndShowErrorDialogMessage is called properly', () => { + describe('when error message in ui handler is UNKNOWN_ERROR', () => { + it('whould show error dialog with given error message', () => { + expect(peerConnection.UIHandler.errorDialogMessage).toEqual( + ErrorMessage.UNKNOWN_ERROR + ); + + const errorMessage = ErrorMessage.DENY_TO_CONNECT; + setAndShowErrorDialogMessage(peerConnection, errorMessage); + + expect( + peerConnection.UIHandler.setDialogErrorMessageCallback + ).toBeCalledWith(errorMessage); + expect(peerConnection.UIHandler.setIsErrorDialogOpen).toBeCalledWith( + true + ); + expect(peerConnection.UIHandler.errorDialogMessage).toEqual( + errorMessage + ); + }); + }); + + describe('when error message in ui handler is NOT UNKNOWN_ERROR', () => { + it('whould not show anything in UI and do nothing to UIHandler', () => { + const originalErrorMessage = ErrorMessage.DISCONNECTED; + peerConnection.UIHandler.errorDialogMessage = originalErrorMessage; + + const errorMessage = ErrorMessage.UNKNOWN_ERROR; + + setAndShowErrorDialogMessage(peerConnection, errorMessage); + + expect( + peerConnection.UIHandler.setDialogErrorMessageCallback + ).not.toBeCalled(); + expect(peerConnection.UIHandler.setIsErrorDialogOpen).not.toBeCalled(); + expect(peerConnection.UIHandler.errorDialogMessage).toEqual( + originalErrorMessage + ); + }); + }); + }); +}); diff --git a/app/client/src/features/PeerConnection/setAndShowErrorDialogMessage.ts b/app/client/src/features/PeerConnection/setAndShowErrorDialogMessage.ts new file mode 100644 index 0000000..5588b29 --- /dev/null +++ b/app/client/src/features/PeerConnection/setAndShowErrorDialogMessage.ts @@ -0,0 +1,12 @@ +import PeerConnection from '.'; +import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum'; + +export default (peerConnection: PeerConnection, errorMessage: ErrorMessage) => { + if ( + peerConnection.UIHandler.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR + ) { + peerConnection.UIHandler.setDialogErrorMessageCallback(errorMessage); + peerConnection.UIHandler.setIsErrorDialogOpen(true); + peerConnection.UIHandler.errorDialogMessage = errorMessage; + } +}; diff --git a/app/client/src/features/PeerConnection/setSdpMediaBitrate.spec.ts b/app/client/src/features/PeerConnection/setSdpMediaBitrate.spec.ts new file mode 100644 index 0000000..af01288 --- /dev/null +++ b/app/client/src/features/PeerConnection/setSdpMediaBitrate.spec.ts @@ -0,0 +1,17 @@ +import { INPUTtestSdpMediaBitrate } from './mocks/INPUTvideo500000testSdpMediaBitrate'; +import { OUTPUTtestSdpMediaBitrate } from './mocks/OUTPUTvideo500000testSdpMediaBitrate'; +import setSdpMediaBitrate from './setSdpMediaBitrate'; + +describe('setSdpMediaBitrate', () => { + describe('when setSdpMediaBitrate is called with sdp input', () => { + it('should produce a result that should match with test sdp string', () => { + const result = setSdpMediaBitrate( + INPUTtestSdpMediaBitrate, + 'video', + 500000 + ); + + expect(result).toMatch(OUTPUTtestSdpMediaBitrate); + }); + }); +}); diff --git a/app/client/src/features/PeerConnection/simplePeerDataMessages.ts b/app/client/src/features/PeerConnection/simplePeerDataMessages.ts new file mode 100644 index 0000000..c97f724 --- /dev/null +++ b/app/client/src/features/PeerConnection/simplePeerDataMessages.ts @@ -0,0 +1,20 @@ +export function prepareDataMessageToChangeQuality(q: number) { + return ` + { + "type": "set_video_quality", + "payload": { + "value": ${q} + } + } + `; +} + +export function prepareDataMessageToGetSharingSourceType(){ + return ` + { + "type": "get_sharing_source_type", + "payload": { + } + } + `; +} diff --git a/app/client/src/features/PeerConnection/startSocketConnectedCheckingLoop/index.spec.ts b/app/client/src/features/PeerConnection/startSocketConnectedCheckingLoop/index.spec.ts new file mode 100644 index 0000000..ae3c003 --- /dev/null +++ b/app/client/src/features/PeerConnection/startSocketConnectedCheckingLoop/index.spec.ts @@ -0,0 +1,62 @@ +import PeerConnection from '..'; +import VideoAutoQualityOptimizer from '../../VideoAutoQualityOptimizer'; +import Crypto from '../../../utils/crypto'; +import startSocketConnectedCheckingLoop from '.'; +import setAndShowErrorDialogMessage from '../setAndShowErrorDialogMessage'; +import PeerConnectionUIHandler from '../PeerConnectionUIHandler'; + +jest.useFakeTimers(); + +jest.mock('../setAndShowErrorDialogMessage'); + +describe('startSocketConnectedCheckingLoop', () => { + let peerConnection: PeerConnection; + + beforeEach(() => { + jest.clearAllMocks(); + + peerConnection = new PeerConnection( + '123', + jest.fn(), + new Crypto(), + new VideoAutoQualityOptimizer(), + new PeerConnectionUIHandler( + true, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn() + ) + ); + + peerConnection.socket = { connected: true }; + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + describe('when interval passed and socket is connected', () => { + it('should NOT call setAndShowErrorDialogMessage', () => { + startSocketConnectedCheckingLoop(peerConnection); + jest.advanceTimersByTime(3000); + + expect(setAndShowErrorDialogMessage).not.toBeCalled(); + }); + }); + + describe('when interval passed and socket is NOT connected', () => { + it('should call setAndShowErrorDialogMessage', () => { + peerConnection.socket.connected = false; + + startSocketConnectedCheckingLoop(peerConnection); + jest.advanceTimersByTime(3000); + + expect(setAndShowErrorDialogMessage).toBeCalled(); + }); + }); +}); diff --git a/app/client/src/features/PeerConnection/startSocketConnectedCheckingLoop/index.ts b/app/client/src/features/PeerConnection/startSocketConnectedCheckingLoop/index.ts new file mode 100644 index 0000000..547ec1c --- /dev/null +++ b/app/client/src/features/PeerConnection/startSocketConnectedCheckingLoop/index.ts @@ -0,0 +1,11 @@ +import PeerConnection from '..'; +import { ErrorMessage } from '../../../components/ErrorDialog/ErrorMessageEnum'; +import setAndShowErrorDialogMessage from '../setAndShowErrorDialogMessage'; + +export default (peerConnection: PeerConnection) => { + setInterval(() => { + if (!peerConnection.socket.connected) { + setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT); + } + }, 2000); +}; diff --git a/app/client/src/features/PeerConnection/VideoQualityEnum.ts b/app/client/src/features/VideoAutoQualityOptimizer/VideoQualityEnum.ts similarity index 100% rename from app/client/src/features/PeerConnection/VideoQualityEnum.ts rename to app/client/src/features/VideoAutoQualityOptimizer/VideoQualityEnum.ts diff --git a/app/client/src/features/VideoAutoQualityOptimizer/errors/CanvasNotDefinedError.ts b/app/client/src/features/VideoAutoQualityOptimizer/errors/CanvasNotDefinedError.ts new file mode 100644 index 0000000..eb9217f --- /dev/null +++ b/app/client/src/features/VideoAutoQualityOptimizer/errors/CanvasNotDefinedError.ts @@ -0,0 +1,7 @@ +export default class CanvasNotDefinedError extends Error { + constructor() { + super('internal variable of canvas DOM elemenent should be defined!'); + // Set the prototype explicitly. + Object.setPrototypeOf(this, CanvasNotDefinedError.prototype); + } +} diff --git a/app/client/src/features/VideoAutoQualityOptimizer/errors/ImageDataIsUndefinedError.ts b/app/client/src/features/VideoAutoQualityOptimizer/errors/ImageDataIsUndefinedError.ts new file mode 100644 index 0000000..203e658 --- /dev/null +++ b/app/client/src/features/VideoAutoQualityOptimizer/errors/ImageDataIsUndefinedError.ts @@ -0,0 +1,7 @@ +export default class ImageDataIsUndefinedError extends Error { + constructor() { + super('imageData retreived is undefined!'); + // Set the prototype explicitly. + Object.setPrototypeOf(this, ImageDataIsUndefinedError.prototype); + } +} diff --git a/app/client/src/features/VideoAutoQualityOptimizer/errors/VideoDimensionsAreWrongError.ts b/app/client/src/features/VideoAutoQualityOptimizer/errors/VideoDimensionsAreWrongError.ts new file mode 100644 index 0000000..ac84b4b --- /dev/null +++ b/app/client/src/features/VideoAutoQualityOptimizer/errors/VideoDimensionsAreWrongError.ts @@ -0,0 +1,9 @@ +export default class VideoDimensionsAreWrongError extends Error { + constructor() { + super( + 'video dimensions are wrong, neither width nor height can be zero!' + ); + // Set the prototype explicitly. + Object.setPrototypeOf(this, VideoDimensionsAreWrongError.prototype); + } +} diff --git a/app/client/src/features/VideoAutoQualityOptimizer/errors/VideoNotDefinedError.ts b/app/client/src/features/VideoAutoQualityOptimizer/errors/VideoNotDefinedError.ts new file mode 100644 index 0000000..48d1b8a --- /dev/null +++ b/app/client/src/features/VideoAutoQualityOptimizer/errors/VideoNotDefinedError.ts @@ -0,0 +1,7 @@ +export default class VideoNotDefinedError extends Error { + constructor() { + super('internal variable of video DOM elemenent should be defined!'); + // Set the prototype explicitly. + Object.setPrototypeOf(this, VideoNotDefinedError.prototype); + } +} diff --git a/app/client/src/features/VideoAutoQualityOptimizer/index.spec.ts b/app/client/src/features/VideoAutoQualityOptimizer/index.spec.ts new file mode 100644 index 0000000..970fc99 --- /dev/null +++ b/app/client/src/features/VideoAutoQualityOptimizer/index.spec.ts @@ -0,0 +1,381 @@ +import VideoAutoQualityOptimizer from '.'; +import { + COMPARISON_CANVAS_ID, + REACT_PLAYER_WRAPPER_ID, +} from '../../constants/appConstants'; +import CanvasNotDefinedError from './errors/CanvasNotDefinedError'; +import ImageDataIsUndefinedError from './errors/ImageDataIsUndefinedError'; +import VideoDimensionsAreWrongError from './errors/VideoDimensionsAreWrongError'; +import VideoNotDefinedError from './errors/VideoNotDefinedError'; + +jest.useFakeTimers(); + +const TEST_IMAGE_DATA = { + data: new Uint8ClampedArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]), + height: 1, + width: 10, +} as ImageData; + +const PREV_FRAME_IMAGE_DATA = { + data: new Uint8ClampedArray([2, 2, 3, 4, 5, 6, 7, 8, 9, 0]), + height: 1, + width: 10, +} as ImageData; + +const LOW_MISMATCH = 0.09; +const HIGH_MISMATCH = 0.2; + +describe('VideoAutoQualityOptimizer', () => { + let optimizer: VideoAutoQualityOptimizer; + + beforeEach(() => { + optimizer = new VideoAutoQualityOptimizer(); + + optimizer.video = { + ...optimizer.video, + videoWidth: 1142, + videoHeight: 1142, + } as HTMLVideoElement; + // @ts-ignore + optimizer.canvas = { + width: 123, + height: 123, + getContext: () => { + return { + clearRect: () => {}, + drawImage: () => {}, + }; + }, + } as HTMLCanvasElement; + }); + + describe('when VideoAutoQualityOptimizer is created properly', () => { + describe('whev optimization loop is running', () => { + it('should call goodQualityCallback when there is small frames mismatch and halfQualityCallback when there is large frames mismatch', () => { + optimizer.prepareCanvasAndVideo = () => {}; + optimizer.validateBeforeCalculations = () => {}; + optimizer.clearCanvas = () => {}; + optimizer.scaleCanvas = () => {}; + optimizer.drawVideoFrameToCanvas = () => {}; + optimizer.getImageDataFromCanvas = () => ({} as ImageData); + optimizer.prevFrame = PREV_FRAME_IMAGE_DATA; + + optimizer.halfQualityCallbak = jest.fn(); + optimizer.goodQualityCallback = jest.fn(); + + // 1. STEP 1 simulate high percent of frames mismatch + optimizer.getPreviousAndCurrentFrameMismatchInPercent = () => 0.5 + optimizer.largeMismatchFramesCount = 5; + optimizer.startOptimizationLoop(); + jest.advanceTimersByTime(5000); + + expect(optimizer.halfQualityCallbak).toBeCalled(); + + // 2. STEP 2 simulate low percent of frames mismatch + optimizer.getPreviousAndCurrentFrameMismatchInPercent = () => 0.03 + optimizer.largeMismatchFramesCount = 0; + optimizer.startOptimizationLoop(); + jest.advanceTimersByTime(5000); + + expect(optimizer.goodQualityCallback).toBeCalled(); + + // 3. repeat step 1 + optimizer.getPreviousAndCurrentFrameMismatchInPercent = () => 0.5 + optimizer.largeMismatchFramesCount = 5; + optimizer.startOptimizationLoop(); + jest.advanceTimersByTime(5000); + + expect(optimizer.halfQualityCallbak).toBeCalled(); + + // 4. repeat step 2 + optimizer.getPreviousAndCurrentFrameMismatchInPercent = () => 0.03 + optimizer.largeMismatchFramesCount = 0; + optimizer.startOptimizationLoop(); + jest.advanceTimersByTime(5000); + + expect(optimizer.goodQualityCallback).toBeCalled(); + + }); + }); + + describe('when validateVideoIsDefined is called', () => { + describe('when video local variable is undefined', () => { + it('should throw an error', () => { + optimizer.video = undefined; + + try { + optimizer.validateVideoIsDefined(); + } catch (e) { + expect(e).toEqual(new VideoNotDefinedError()); + } + }); + }); + }); + + describe('when validateVideoIsDefined is called', () => { + describe('when canvas local variable is undefined', () => { + it('should throw an error', () => { + optimizer.canvas = undefined; + + try { + optimizer.validateCanvasIsDefined(); + } catch (e) { + expect(e).toEqual(new CanvasNotDefinedError()); + } + }); + }); + }); + + describe('when startOptimizationLoop of VideoAutoQualityOptimizer is called', () => { + it('should define canvas and video internal variables', () => { + const originalDocumentQuerySelector = document.querySelector; + const valueForVideo = 'a'; + const valueForCanvas = 'b'; + + try { + document.querySelector = (s: string) => { + if (s === `#${REACT_PLAYER_WRAPPER_ID} > video`) { + return valueForVideo; + } + if (s === `#${COMPARISON_CANVAS_ID}`) { + return valueForCanvas; + } + return ''; + }; + + optimizer.startOptimizationLoop(); + jest.advanceTimersByTime(2000); + } catch (e) { + expect(optimizer.video).toBe(valueForVideo); + expect(optimizer.canvas).toBe(valueForCanvas); + } + + document.querySelector = originalDocumentQuerySelector; + }); + }); + + describe('when doFrameComparisonAndQualityOptimization of VideoAutoQualityOptimizer is called', () => { + describe('when width of video is 0', () => { + it('should throw an error', () => { + optimizer.video = { + ...optimizer.video, + videoWidth: 0, + videoHeight: 1142, + } as HTMLVideoElement; + try { + optimizer.doFrameComparisonAndQualityOptimization(); + fail('should have thrown error here!'); + } catch (e) { + expect(e).toEqual(new VideoDimensionsAreWrongError()); + } + }); + }); + + describe('when height of video is 0', () => { + it('should throw an error', () => { + optimizer.video = { + ...optimizer.video, + videoWidth: 1142, + videoHeight: 0, + } as HTMLVideoElement; + try { + optimizer.doFrameComparisonAndQualityOptimization(); + fail('should have thrown error here!'); + } catch (e) { + expect(e).toEqual(new VideoDimensionsAreWrongError()); + } + }); + + it('should call scaleCanvas()', () => { + const spy = jest.spyOn(optimizer, 'scaleCanvas'); + + try { + optimizer.doFrameComparisonAndQualityOptimization(); + } catch (e) {} + + expect(spy).toBeCalled(); + }); + }); + + it('should call drawVideoFrameToCanvas()', () => { + const spy = jest.spyOn(optimizer, 'drawVideoFrameToCanvas'); + + try { + optimizer.doFrameComparisonAndQualityOptimization(); + } catch (e) {} + + expect(spy).toBeCalled(); + }); + + describe('when getImageDataFromCanvas returns undefined', () => { + it('should throw an error', () => { + jest + .spyOn(optimizer, 'getImageDataFromCanvas') + .mockImplementation(() => undefined); + + try { + optimizer.doFrameComparisonAndQualityOptimization(); + fail('it should have thrown an error here'); + } catch (e) { + expect(e).toEqual(new ImageDataIsUndefinedError()); + } + }); + }); + + describe('when prevFrame is undefined', () => { + it('should set prevFrame to be the same as test image data', () => { + jest + .spyOn(optimizer, 'getImageDataFromCanvas') + .mockImplementation(() => TEST_IMAGE_DATA); + + try { + optimizer.doFrameComparisonAndQualityOptimization(); + } catch (e) {} + + expect(optimizer.prevFrame).toEqual(TEST_IMAGE_DATA); + }); + }); + + describe('when getPreviousAndCurrentFrameMismatchInPercent returns a number', () => { + it('should call handleFramesMismatch()', () => { + const TEST_MISMATCH = 0.1; + optimizer.prevFrame = TEST_IMAGE_DATA; + jest + .spyOn(optimizer, 'getImageDataFromCanvas') + .mockImplementation(() => TEST_IMAGE_DATA); + jest + .spyOn(optimizer, 'getPreviousAndCurrentFrameMismatchInPercent') + .mockImplementation(() => 0.1); + const spyOfHandleFramesMismatch = jest.spyOn( + optimizer, + 'handleFramesMismatch' + ); + + optimizer.doFrameComparisonAndQualityOptimization(); + + expect(spyOfHandleFramesMismatch).toBeCalledWith(TEST_MISMATCH); + }); + }); + + describe('when doFrameComparisonAndQualityOptimization run was successful', () => { + it('should set prevFrame to test image data', () => { + optimizer.prevFrame = PREV_FRAME_IMAGE_DATA; + jest + .spyOn(optimizer, 'getImageDataFromCanvas') + .mockImplementation(() => TEST_IMAGE_DATA); + jest + .spyOn(optimizer, 'getPreviousAndCurrentFrameMismatchInPercent') + .mockImplementation(() => 0.1); + + optimizer.doFrameComparisonAndQualityOptimization(); + + expect(optimizer.prevFrame).toEqual(TEST_IMAGE_DATA); + }); + }); + + describe('when getPreviousAndCurrentFrameMismatchInPercent run was successful', () => {}); + }); + + describe('when getPreviousAndCurrentFrameMismatchInPercent was called', () => { + describe('when canvas is undefined', () => { + it('should return 0', () => { + optimizer.canvas = undefined; + + const res = optimizer.getPreviousAndCurrentFrameMismatchInPercent( + TEST_IMAGE_DATA + ); + + expect(res).toBe(0); + }); + }); + + describe('when getPreviousAndCurrentFrameMismatchInPercent ran properly', () => { + it('should return proper mismatch in percent', () => { + const MISMATCH_TO_RETURN = 44; + jest + .spyOn(optimizer, 'getNumberOfMismatchedPixels') + .mockImplementation(() => MISMATCH_TO_RETURN); + // @ts-ignore + optimizer.canvas = { + width: 123, + height: 123, + }; + const width = optimizer.canvas?.width as number; + const height = optimizer.canvas?.height as number; + const expected = MISMATCH_TO_RETURN / (width * height); + + const res = optimizer.getPreviousAndCurrentFrameMismatchInPercent( + TEST_IMAGE_DATA + ); + + expect(res).toBe(expected); + }); + }); + }); + + describe('when handleFramesMismatch was called', () => { + describe('when it received low percent mismatch and largeMismatchFramesCount is more than zero', () => { + it('should decrease largeMismatchFramesCount by one', () => { + const MISMATCH_FRAMES_COUNT = 2; + optimizer.largeMismatchFramesCount = MISMATCH_FRAMES_COUNT; + optimizer.handleFramesMismatch(LOW_MISMATCH); + const expected = MISMATCH_FRAMES_COUNT - 1; + + expect(optimizer.largeMismatchFramesCount).toBe(expected); + }); + }); + + describe('when it received LOW percent mismatch and isRequestedHalfQuality is true', () => { + it('should call goodQualityCallback and set isRequestedHalfQuality to false', () => { + const MISMATCH_FRAMES_COUNT = 0; + optimizer.largeMismatchFramesCount = MISMATCH_FRAMES_COUNT; + optimizer.isRequestedHalfQuality = true; + optimizer.goodQualityCallback = jest.fn(); + optimizer.handleFramesMismatch(LOW_MISMATCH); + + expect(optimizer.goodQualityCallback).toBeCalled(); + expect(optimizer.isRequestedHalfQuality).toBe(false); + }); + }); + + describe('when it received LOW percent mismatch and isRequestedHalfQuality is false', () => { + it('should call NOT goodQualityCallback', () => { + const MISMATCH_FRAMES_COUNT = 0; + optimizer.largeMismatchFramesCount = MISMATCH_FRAMES_COUNT; + optimizer.isRequestedHalfQuality = false; + optimizer.goodQualityCallback = jest.fn(); + optimizer.handleFramesMismatch(LOW_MISMATCH); + + expect(optimizer.goodQualityCallback).not.toBeCalled(); + }); + }); + + describe('when it received HIGH percent mismatch', () => { + describe('when isRequestedHalfQuality is false and there were MORE than consecutive 3 frames mismatch', () => { + it('should call halfQualityCallbak and set isRequestedHalfQuality to true', () => { + const MISMATCH_FRAMES_COUNT = 3; + optimizer.largeMismatchFramesCount = MISMATCH_FRAMES_COUNT; + optimizer.isRequestedHalfQuality = false; + optimizer.halfQualityCallbak = jest.fn(); + optimizer.handleFramesMismatch(HIGH_MISMATCH); + + expect(optimizer.halfQualityCallbak).toBeCalled(); + expect(optimizer.isRequestedHalfQuality).toBe(true); + }); + }); + + describe('when isRequestedHalfQuality is false and there were LESS than consecutive 3 frames mismatch', () => { + it('should increase largeMismatchFramesCount by one', () => { + const MISMATCH_FRAMES_COUNT = 1; + optimizer.largeMismatchFramesCount = MISMATCH_FRAMES_COUNT; + optimizer.isRequestedHalfQuality = false; + optimizer.handleFramesMismatch(HIGH_MISMATCH); + const expected = MISMATCH_FRAMES_COUNT + 1; + + expect(optimizer.largeMismatchFramesCount).toBe(expected); + }); + }); + }); + }); + }); +}); diff --git a/app/client/src/features/VideoAutoQualityOptimizer/index.ts b/app/client/src/features/VideoAutoQualityOptimizer/index.ts index 8205c01..674aba6 100644 --- a/app/client/src/features/VideoAutoQualityOptimizer/index.ts +++ b/app/client/src/features/VideoAutoQualityOptimizer/index.ts @@ -1,12 +1,20 @@ +import { COMPARISON_CANVAS_ID } from './../../constants/appConstants'; import pixelmatch from 'pixelmatch'; import { REACT_PLAYER_WRAPPER_ID } from '../../constants/appConstants'; +import CanvasNotDefinedError from './errors/CanvasNotDefinedError'; +import VideoDimensionsAreWrongError from './errors/VideoDimensionsAreWrongError'; +import VideoNotDefinedError from './errors/VideoNotDefinedError'; +import ImageDataIsUndefinedError from './errors/ImageDataIsUndefinedError'; + +export const CANVAS_SCALE_MULTIPLIER = 0.125; // 1/8 of original canvas size, to speed up calculations +export const MISMATCH_PERCENT_THRESHOLD = 0.1; export default class VideoAutoQualityOptimizer { - video: any; + video: undefined | HTMLVideoElement; - canvas: any; + canvas: undefined | HTMLCanvasElement; - prevFrame: any; + prevFrame: undefined | ImageData; largeMismatchFramesCount = 0; @@ -25,66 +33,158 @@ export default class VideoAutoQualityOptimizer { } startOptimizationLoop() { + this.prepareCanvasAndVideo(); setInterval(() => { - this.frameComparisonQualityOptimization(); - }, 1000); - } - - frameComparisonQualityOptimization() { - if (this.video && this.canvas) { - if (this.video.videoWidth === 0 || this.video.videoHeight === 0) return; - // scale the canvas accordingly - this.canvas - .getContext('2d') - .clearRect(0, 0, this.canvas.width, this.canvas.height); - this.canvas.width = this.video.videoWidth / 8; - this.canvas.height = this.video.videoHeight / 8; - // draw the video at that frame - this.canvas - .getContext('2d') - .drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height); - // convert it to a usable data URL - let imageData = this.canvas - .getContext('2d') - .getImageData(0, 0, this.canvas.width, this.canvas.height); - - if (this.prevFrame) { - try { - const numMismatchedPixels = pixelmatch( - this.prevFrame.data, - imageData.data, - null, - this.canvas.width, - this.canvas.height, - { threshold: 0.1 } - ); - const mismatchInPercent = - numMismatchedPixels / (this.canvas.width * this.canvas.height); - if (mismatchInPercent < 0.1 && this.largeMismatchFramesCount > 0) { - this.largeMismatchFramesCount -= 1; - } else if (mismatchInPercent < 0.1 && this.isRequestedHalfQuality) { - this.largeMismatchFramesCount = 0; - this.isRequestedHalfQuality = false; - this.goodQualityCallback(); - } else if (mismatchInPercent >= 0.1 && !this.isRequestedHalfQuality) { - if (this.largeMismatchFramesCount < 3) { - this.largeMismatchFramesCount += 1; - } else { - this.halfQualityCallbak(); - this.isRequestedHalfQuality = true; - } - } - } catch (e) { + try { + this.doFrameComparisonAndQualityOptimization(); + } catch (e) { + // some errors may be thrown here, better ignore them in production + if (process.env.NODE_ENV === 'development') { console.error(e); } } + }, 1000); + } + + doFrameComparisonAndQualityOptimization() { + this.validateBeforeCalculations(); + this.clearCanvas(); + this.scaleCanvas(); + this.drawVideoFrameToCanvas(); + let imageData = this.getImageDataFromCanvas(); + + if (!imageData) { + throw new ImageDataIsUndefinedError(); + } + if (!this.prevFrame) { this.prevFrame = imageData; - imageData = null; - } else { - this.video = document.querySelector( - `#${REACT_PLAYER_WRAPPER_ID} > video` - ); - this.canvas = document.getElementById('comparison-canvas'); + return; + } + + try { + const mismatchInPercent = this.getPreviousAndCurrentFrameMismatchInPercent(imageData); + this.handleFramesMismatch(mismatchInPercent); + } catch (e) { + // usually frames size mismatch thrown here, so can be ignored as it happens + // often when changing sharing window size + // so logging this error may be not necessary + } + + this.prevFrame = imageData; + } + + findAndSetVideoInternalVariable(document: Document) { + this.video = document.querySelector( + `#${REACT_PLAYER_WRAPPER_ID} > video` + ) as HTMLVideoElement; + } + + findAndSetCanvasInternalVariable(document: Document) { + this.canvas = document.querySelector( + `#${COMPARISON_CANVAS_ID}` + ) as HTMLCanvasElement; + } + + prepareCanvasAndVideo() { + setTimeout(() => { + this.findAndSetVideoInternalVariable(document); + this.findAndSetCanvasInternalVariable(document); + }, 1000); + } + + clearCanvas() { + this.canvas + ?.getContext('2d') + ?.clearRect(0, 0, this.canvas.width, this.canvas.height); + } + + validateVideoWidthAndHeight() { + if (this.video?.videoWidth === 0 || this.video?.videoHeight === 0) { + throw new VideoDimensionsAreWrongError(); } } + + validateBeforeCalculations() { + this.validateVideoWidthAndHeight(); + this.validateVideoIsDefined(); + this.validateCanvasIsDefined(); + } + + validateVideoIsDefined() { + if (!this.video) { + throw new VideoNotDefinedError(); + } + } + + validateCanvasIsDefined() { + if (!this.canvas) { + throw new CanvasNotDefinedError(); + } + } + + scaleCanvas() { + if ( + !this.canvas || + !this.video + ) + return; + this.canvas.width = this.video.videoWidth * CANVAS_SCALE_MULTIPLIER; + this.canvas.height = this.video.videoHeight * CANVAS_SCALE_MULTIPLIER; + } + + drawVideoFrameToCanvas() { + if (!this.video) return; + this.canvas + ?.getContext('2d') + ?.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height); + } + + getImageDataFromCanvas() { + return this.canvas + ?.getContext('2d') + ?.getImageData(0, 0, this.canvas.width, this.canvas.height); + } + + getNumberOfMismatchedPixels(imageData: ImageData) { + if (!this.canvas || !this.canvas.width || !this.prevFrame) return 0; + return pixelmatch( + this.prevFrame.data, + imageData.data, + null, + this.canvas.width, + this.canvas.height, + { threshold: 0.1 } + ); + } + + getPreviousAndCurrentFrameMismatchInPercent(imageData: ImageData) { + if (!this.canvas) return 0; + return this.getNumberOfMismatchedPixels(imageData) / (this.canvas.width * this.canvas.height); + } + + handleFramesMismatch(mismatchInPercent: number) { + if (mismatchInPercent < 0.1 && this.largeMismatchFramesCount > 0) { + this.largeMismatchFramesCount -= 1; + } else if (mismatchInPercent < 0.1 && this.isRequestedHalfQuality) { + this.largeMismatchFramesCount = 0; + this.isRequestedHalfQuality = false; + this.goodQualityCallback(); + } else if (mismatchInPercent >= 0.1 && !this.isRequestedHalfQuality) { + if (this.largeMismatchFramesCount < 3) { + this.largeMismatchFramesCount += 1; + } else { + this.halfQualityCallbak(); + this.isRequestedHalfQuality = true; + } + } + } + + + isLowMismatchPercent(mismatchInPercent: number) { + return mismatchInPercent < MISMATCH_PERCENT_THRESHOLD; + } + + isHighMismatchPercent(mismatchInPercent: number) { + return mismatchInPercent >= MISMATCH_PERCENT_THRESHOLD; + } } diff --git a/app/client/src/providers/AppContextProvider/__snapshots__/index.spec.tsx.snap b/app/client/src/providers/AppContextProvider/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000..cbdccae --- /dev/null +++ b/app/client/src/providers/AppContextProvider/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should match exact snapshot 1`] = `null`; diff --git a/app/client/src/providers/AppContextProvider/index.spec.tsx b/app/client/src/providers/AppContextProvider/index.spec.tsx new file mode 100644 index 0000000..fcc3cd7 --- /dev/null +++ b/app/client/src/providers/AppContextProvider/index.spec.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { AppContextProvider } from '.'; + +jest.useFakeTimers(); + +it('should match exact snapshot', () => { + const subject = renderer.create( + <> + + + ); + expect(subject).toMatchSnapshot(); +}); diff --git a/app/client/src/react-app-env.d.ts b/app/client/src/react-app-env.d.ts index 6431bc5..8488ec6 100644 --- a/app/client/src/react-app-env.d.ts +++ b/app/client/src/react-app-env.d.ts @@ -1 +1,27 @@ /// + +type ConnectionIconType = 'feed' | 'feed-subscribed'; +type LoadingSharingIconType = 'desktop' | 'application'; +type ScreenSharingSourceType = 'screen' | 'window'; +type CreatePeerConnectionUseEffectParams = { + isDarkTheme: boolean; + peer: undefined | PeerConnection; + setIsDarkThemeHook: (_: boolean) => void; + setMyDeviceDetails: (_: DeviceDetails) => void; + setConnectionIconType: (_: ConnectionIconType) => void; + setIsShownTextPrompt: (_: boolean) => void; + setPromptStep: (_: number) => void; + setScreenSharingSourceType: (_: ScreenSharingSourceType) => void; + setDialogErrorMessage: (_: ErrorMessage) => void; + setIsErrorDialogOpen: (_: boolean) => void; + setUrl: (_: MediaStream) => void; + setPeer: (_: PeerConnection) => void; +}; +type handleDisplayingLoadingSharingIconLoopParams = { + promptStep: number; + url: undefined | MediaStream; + setIsShownLoadingSharingIcon: (_: boolean) => void; + loadingSharingIconType: LoadingSharingIconType; + isShownLoadingSharingIcon: boolean; + setLoadingSharingIconType: (_: LoadingSharingIconType) => void; +}; diff --git a/app/client/src/utils/ProcessedMessage.d.ts b/app/client/src/utils/ProcessedMessage.d.ts index 2a28b9b..61bee95 100644 --- a/app/client/src/utils/ProcessedMessage.d.ts +++ b/app/client/src/utils/ProcessedMessage.d.ts @@ -1,5 +1,5 @@ -type CallAcceptedMessageWithPayload = { - type: 'CALL_ACCEPTED'; +type CallUserMessageWithPayload = { + type: 'CALL_USER'; payload: { signalData: string; }; @@ -17,6 +17,41 @@ type DeviceDetailsMessageWithPayload = { }; }; +type DenyToConnectMessageWithPayload = { + type: 'DENY_TO_CONNECT'; + payload: {}; +}; + +type DisconnectByHostMachineUserMessageWithPayload = { + type: 'DISCONNECT_BY_HOST_MACHINE_USER'; + payload: {}; +}; + +type AllowedToConnectMessageWithPayload = { + type: 'ALLOWED_TO_CONNECT'; + payload: {}; +}; + +type AppThemeMessageWithPayload = { + type: 'APP_THEME'; + payload: { + value: boolean, + }; +}; + +type AppLanguageMessageWithPayload = { + type: 'APP_LANGUAGE'; + payload: { + value: string, + }; +}; + + type ProcessedMessage = - | CallAcceptedMessageWithPayload - | DeviceDetailsMessageWithPayload; + | CallUserMessageWithPayload + | DeviceDetailsMessageWithPayload + | DenyToConnectMessageWithPayload + | DisconnectByHostMachineUserMessageWithPayload + | AllowedToConnectMessageWithPayload + | AppThemeMessageWithPayload + | AppLanguageMessageWithPayload; diff --git a/app/client/src/utils/areWeTestingWithJest.ts b/app/client/src/utils/areWeTestingWithJest.ts new file mode 100644 index 0000000..8039c16 --- /dev/null +++ b/app/client/src/utils/areWeTestingWithJest.ts @@ -0,0 +1,3 @@ +export default () => { + return process.env.JEST_WORKER_ID !== undefined; +}; diff --git a/app/client/src/utils/getRoomIDOfCurrentBrowserWindow.ts b/app/client/src/utils/getRoomIDOfCurrentBrowserWindow.ts new file mode 100644 index 0000000..2fd04f9 --- /dev/null +++ b/app/client/src/utils/getRoomIDOfCurrentBrowserWindow.ts @@ -0,0 +1,3 @@ +export default () => { + return encodeURI(window.location.pathname.replace('/', '')); +} diff --git a/app/client/src/utils/message.spec.ts b/app/client/src/utils/message.spec.ts new file mode 100644 index 0000000..df4b2b7 --- /dev/null +++ b/app/client/src/utils/message.spec.ts @@ -0,0 +1,53 @@ +import { prepare, process } from './message'; +import getTestPublickKeyPem from './mocks/getTestPublicKeyPem'; +import getTestPrivateKeyPem from './mocks/getTestPrivateKeyPem'; + +interface TestPayload { + text: string; +} + +describe('message.ts tests for proper encryption and decryption functionality', () => { + const TEST_TEXT = 'some test text here'; + const TEST_TEXT_AS_URL = 'some%20test%20text%20here'; + const testPayloadToEncrypt = { + payload: { + text: TEST_TEXT, + }, + }; + + const testUser = { + username: 'testUsername', + id: 'testId', + }; + + const testPartner = { + publicKey: getTestPublickKeyPem(), + }; + it('should create encrypted payload with prepare() method', async () => { + const encryptedPayload = await prepare( + testPayloadToEncrypt, + testUser, + testPartner + ); + + expect(encryptedPayload.toSend.payload).not.toContain(TEST_TEXT); + expect(encryptedPayload.toSend.payload).not.toContain(TEST_TEXT_AS_URL); + }); + + it('should decrypt encrypted payload with process() method', async () => { + const encryptedPayload = await prepare( + testPayloadToEncrypt, + testUser, + testPartner + ); + + const decryptedPayload = await process( + encryptedPayload.toSend, + getTestPrivateKeyPem() + ); + + expect( + ((decryptedPayload.payload as unknown) as TestPayload).text + ).toContain(TEST_TEXT_AS_URL); + }); +}); diff --git a/app/client/src/utils/message.ts b/app/client/src/utils/message.ts index b91dadb..2e4215d 100644 --- a/app/client/src/utils/message.ts +++ b/app/client/src/utils/message.ts @@ -42,7 +42,7 @@ export const process = (payload: any, privateKeyString: string) => key.sessionKey ); signingHMACKey = crypto.unwrapKey(privateKey, key.signingKey); - resolvePayload(); + resolvePayload(undefined); } catch (e) { console.error(e); } diff --git a/app/client/src/utils/mocks/getTestPrivateKeyPem.ts b/app/client/src/utils/mocks/getTestPrivateKeyPem.ts new file mode 100644 index 0000000..33b5d0b --- /dev/null +++ b/app/client/src/utils/mocks/getTestPrivateKeyPem.ts @@ -0,0 +1,29 @@ +export default function getTestPrivateKeyPem() { + return `-----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEAoQ8+vdVAerEyaxGaffzrTTv+sgpQ3xToBFYkxrT6f+zP4MqV + nTIy2+UMlEGGryMFgJWyurqv+NyoVf7vIFOxQcxJko1BL/oIt/e5YZ/fMIw9AhgP + 0A0oZyFKNbGesCY3zMdpZqPE0brzfhjr/lu5VzioZI9vxocOSSc3+S8w1EXqujgO + X3PWXgZrD6Y//Oo+8BgNQta/e5PUyc9yNchU/W3ddzBdE0iUXTdQt7yFvzy4vTbS + ywUxoNNJnD6dUJ6dZ4c24VGvrvhJ5mzNqQjibAhtkPFbvhn0e0BHl0BZ876fLHGg + zpHgmmiWbKwBYl1ydv8fW9W3r5nQoW0RThYJjwIDAQABAoIBAHqOrUGrGsvCNwl+ + db9VTICTHLbCXtPChuN14bpLUSszOuRlhAAAiO8HltDiI+j1j2RPhZfOI8YNsxLt + UW2aAhJ9r6aTUn19mFDVcv20uBOrQ2lqge3hdVM049GD/asxCdkMDUqLaGPoDQ1x + TXNavOiANrN+6qF5eAd2joNRw6hi7osyWqfpgM9y58kiYYazKHKlI/er19JsD2t5 + hgRiFti+oN9nqhhVzJI/GYU9JugXnB/Z0uFvqOyt/3YDHPpOC07WYpfA3yzshBkW + TiiMp1eJNX/sRh4JHzRI9/MQe9ajlAS7T74HwCkclzaS8ZY6t+dfVZ097/ug77eQ + NwY4DAECgYEA0hWv7hpookKKcl0srfxbx+FbtBhzXqz+Tfy3H5do6uXRc0aUwQ25 + PZLs7UAT3zjt+4aYu1xgkp3cUx5FNr/FAPYLtjXw1/6fmt3HX8820SpCMjIzS1bU + UwJvmAut70YA+n2eKwO1qJNeLcpNCDebbIXwj0lKDh5HQDyoswuz7I8CgYEAxEKV + N4Ma6GqZ0hgdQlO71oSuftHUI80Iz+Riorrp4AA8hp5A3V6Fd9QDy8PYLz1kD9pM + uWdCLqaxdOVDtLVnjNVZH1SI+wSBDA/0OtdvPaONL1JKRA21kol049f0M8wkulAw + 6lfi2aQwZ3KWyEAfDy5iPdVROnQWqUcLmxO0kwECgYAGpSr4dBtlLoekkG/uXPIm + Q2mcK73Se9RbcSf1tttZusVCSTRBWwbF/NTDuGgogmt8rkg8fPKNELM8adO0pKI9 + oorCS7h/jI1N38ADttE8EoMfhVj8BBYZPhV7kLsCu4siYUDUiXyAhZDQD/sZzHB9 + IUt3rNDL24dTb9fCOheJ3wKBgGRgpY7Z2DZM51VkDfrxdp3WCKVGTljtMfeaGLSg + IqP1mv9DC2vtPxg1cKeUCArJPFc7UIh2/ot7qEFgTQusyERojgePJew0tofj1QcP + To7ZConMbb12wYosEYPC3NxtKc+82ffRcW3dIwCVw/axjPEnyQlVBBGAdGKpuo7b + Oj0BAoGAMs2zuAJfJB4xE1UZdLXk9uHWPAzgUNDzHuS5mMWuloGvKX+19yck6o2J + ZA/iq6GwOgvD2y9MC0mV4WkmwZpVXwPVpZD8GVQXZf2xZgh/q2e3IObAd80NLnaG + v86qN98DRTh9L+47Nsaf1J5vDDaAfH2Ir8UgAQ5ZMEFDm7P0hUQ= + -----END RSA PRIVATE KEY-----`; +} diff --git a/app/utils/mocks/getTestPublickKeyPem.ts b/app/client/src/utils/mocks/getTestPublicKeyPem.ts similarity index 100% rename from app/utils/mocks/getTestPublickKeyPem.ts rename to app/client/src/utils/mocks/getTestPublicKeyPem.ts diff --git a/app/client/src/utils/userAgentParserHelpers.ts b/app/client/src/utils/userAgentParserHelpers.ts index 2df0a1c..3ace6c9 100644 --- a/app/client/src/utils/userAgentParserHelpers.ts +++ b/app/client/src/utils/userAgentParserHelpers.ts @@ -1,3 +1,4 @@ +// @ts-ignore import { UAParser } from 'ua-parser-js'; export function getOSFromUAParser(uaParser: UAParser) { diff --git a/app/client/tsconfig.json b/app/client/tsconfig.json index b73715e..bbdebcc 100644 --- a/app/client/tsconfig.json +++ b/app/client/tsconfig.json @@ -17,7 +17,7 @@ "isolatedModules": true, "noEmit": true, "jsx": "react", - "strict": true + "strict": true, }, "include": [ "src" diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 0f64c2b..47415bd 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -1648,13 +1648,6 @@ dependencies: "@babel/types" "^7.3.0" -"@types/cheerio@^0.22.22": - version "0.22.23" - resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.23.tgz#74bcfee9c5ee53f619711dca953a89fe5cfa4eb4" - integrity sha512-QfHLujVMlGqcS/ePSf3Oe5hK3H8wi/yN2JYuxSB1U10VvW1fO3K8C+mURQesFYS1Hn7lspOsTT75SKq/XtydQg== - dependencies: - "@types/node" "*" - "@types/dom4@^2.0.1": version "2.0.1" resolved "https://registry.npmjs.org/@types/dom4/-/dom4-2.0.1.tgz#506d5781b9bcab81bd9a878b198aec7dee2a6033" @@ -2129,21 +2122,6 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -airbnb-prop-types@^2.16.0: - version "2.16.0" - resolved "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" - integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== - dependencies: - array.prototype.find "^2.1.1" - function.prototype.name "^1.1.2" - is-regex "^1.1.0" - object-is "^1.1.2" - object.assign "^4.1.0" - object.entries "^1.1.2" - prop-types "^15.7.2" - prop-types-exact "^1.2.0" - react-is "^16.13.1" - ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -2299,11 +2277,6 @@ array-equal@^1.0.0: resolved "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= -array-filter@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" - integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -2342,15 +2315,7 @@ array-unique@^0.3.2: resolved "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.find@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c" - integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.4" - -array.prototype.flat@^1.2.1, array.prototype.flat@^1.2.3: +array.prototype.flat@^1.2.1: version "1.2.4" resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg== @@ -3136,30 +3101,6 @@ chardet@^0.7.0: resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -cheerio-select-tmp@^0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646" - integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ== - dependencies: - css-select "^3.1.2" - css-what "^4.0.0" - domelementtype "^2.1.0" - domhandler "^4.0.0" - domutils "^2.4.4" - -cheerio@^1.0.0-rc.3: - version "1.0.0-rc.5" - resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f" - integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw== - dependencies: - cheerio-select-tmp "^0.1.0" - dom-serializer "~1.2.0" - domhandler "^4.0.0" - entities "~2.1.0" - htmlparser2 "^6.0.0" - parse5 "^6.0.0" - parse5-htmlparser2-tree-adapter "^6.0.0" - chokidar@^2.1.8: version "2.1.8" resolved "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -3366,7 +3307,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.19.0, commander@^2.20.0: +commander@^2.11.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -3722,17 +3663,6 @@ css-select@^2.0.0: domutils "^1.7.0" nth-check "^1.0.2" -css-select@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" - integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== - dependencies: - boolbase "^1.0.0" - css-what "^4.0.0" - domhandler "^4.0.0" - domutils "^2.4.3" - nth-check "^2.0.0" - css-tree@1.0.0-alpha.37: version "1.0.0-alpha.37" resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" @@ -3759,11 +3689,6 @@ css-what@^3.2.1: resolved "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== -css-what@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" - integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== - css.escape@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" @@ -4098,11 +4023,6 @@ dir-glob@2.0.0: arrify "^1.0.1" path-type "^3.0.0" -discontinuous-range@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" - integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= - dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -4177,15 +4097,6 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" -dom-serializer@^1.0.1, dom-serializer@~1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" - integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - entities "^2.0.0" - dom4@^2.1.5: version "2.1.6" resolved "https://registry.npmjs.org/dom4/-/dom4-2.1.6.tgz#c90df07134aa0dbd81ed4d6ba1237b36fc164770" @@ -4201,7 +4112,7 @@ domelementtype@1, domelementtype@^1.3.1: resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== -domelementtype@^2.0.1, domelementtype@^2.1.0: +domelementtype@^2.0.1: version "2.1.0" resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== @@ -4220,13 +4131,6 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -domhandler@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" - integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== - dependencies: - domelementtype "^2.1.0" - domutils@1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" @@ -4243,15 +4147,6 @@ domutils@^1.5.1, domutils@^1.7.0: dom-serializer "0" domelementtype "1" -domutils@^2.4.3, domutils@^2.4.4: - version "2.4.4" - resolved "https://registry.npmjs.org/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" - integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.0.1" - domhandler "^4.0.0" - dot-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -4397,84 +4292,11 @@ entities@^1.1.1: resolved "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== -entities@^2.0.0, entities@~2.1.0: +entities@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== -enzyme-adapter-react-16@^1.15.5: - version "1.15.5" - resolved "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.5.tgz#7a6f0093d3edd2f7025b36e7fbf290695473ee04" - integrity sha512-33yUJGT1nHFQlbVI5qdo5Pfqvu/h4qPwi1o0a6ZZsjpiqq92a3HjynDhwd1IeED+Su60HDWV8mxJqkTnLYdGkw== - dependencies: - enzyme-adapter-utils "^1.13.1" - enzyme-shallow-equal "^1.0.4" - has "^1.0.3" - object.assign "^4.1.0" - object.values "^1.1.1" - prop-types "^15.7.2" - react-is "^16.13.1" - react-test-renderer "^16.0.0-0" - semver "^5.7.0" - -enzyme-adapter-utils@^1.13.1: - version "1.14.0" - resolved "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz#afbb0485e8033aa50c744efb5f5711e64fbf1ad0" - integrity sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg== - dependencies: - airbnb-prop-types "^2.16.0" - function.prototype.name "^1.1.3" - has "^1.0.3" - object.assign "^4.1.2" - object.fromentries "^2.0.3" - prop-types "^15.7.2" - semver "^5.7.1" - -enzyme-shallow-equal@^1.0.1, enzyme-shallow-equal@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e" - integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q== - dependencies: - has "^1.0.3" - object-is "^1.1.2" - -enzyme-to-json@^3.6.1: - version "3.6.1" - resolved "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.6.1.tgz#d60740950bc7ca6384dfe6fe405494ec5df996bc" - integrity sha512-15tXuONeq5ORoZjV/bUo2gbtZrN2IH+Z6DvL35QmZyKHgbY1ahn6wcnLd9Xv9OjiwbAXiiP8MRZwbZrCv1wYNg== - dependencies: - "@types/cheerio" "^0.22.22" - lodash "^4.17.15" - react-is "^16.12.0" - -enzyme@^3.11.0: - version "3.11.0" - resolved "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28" - integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw== - dependencies: - array.prototype.flat "^1.2.3" - cheerio "^1.0.0-rc.3" - enzyme-shallow-equal "^1.0.1" - function.prototype.name "^1.1.2" - has "^1.0.3" - html-element-map "^1.2.0" - is-boolean-object "^1.0.1" - is-callable "^1.1.5" - is-number-object "^1.0.4" - is-regex "^1.0.5" - is-string "^1.0.5" - is-subset "^0.1.1" - lodash.escape "^4.0.1" - lodash.isequal "^4.5.0" - object-inspect "^1.7.0" - object-is "^1.0.2" - object.assign "^4.1.0" - object.entries "^1.1.1" - object.values "^1.1.1" - raf "^3.4.1" - rst-selector-parser "^2.2.3" - string.prototype.trim "^1.2.1" - errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -4489,7 +4311,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4: +es-abstract@^1.17.0-next.1, es-abstract@^1.17.2: version "1.17.7" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== @@ -5341,26 +5163,11 @@ function-bind@^1.1.1: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function.prototype.name@^1.1.2, function.prototype.name@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.3.tgz#0bb034bb308e7682826f215eb6b2ae64918847fe" - integrity sha512-H51qkbNSp8mtkJt+nyW1gyStBiKZxfRqySNUR99ylq6BPXHKI4SEvIlTKp4odLfjRKJV04DFWMU3G/YRlQOsag== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - functions-have-names "^1.2.1" - functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -functions-have-names@^1.2.1: - version "1.2.2" - resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21" - integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA== - gensync@^1.0.0-beta.1: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -5672,13 +5479,6 @@ html-comment-regex@^1.1.0: resolved "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== -html-element-map@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22" - integrity sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw== - dependencies: - array-filter "^1.0.0" - html-encoding-sniffer@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" @@ -5740,16 +5540,6 @@ htmlparser2@^3.3.0: inherits "^2.0.1" readable-stream "^3.1.1" -htmlparser2@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01" - integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - domutils "^2.4.4" - entities "^2.0.0" - http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -6108,19 +5898,12 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.0.1: - version "1.1.0" - resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" - integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== - dependencies: - call-bind "^1.0.0" - is-buffer@^1.0.2, is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.2: +is-callable@^1.1.4, is-callable@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== @@ -6249,11 +6032,6 @@ is-negative-zero@^2.0.0: resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== -is-number-object@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== - is-number@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -6307,7 +6085,7 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1: +is-regex@^1.0.4, is-regex@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== @@ -6339,11 +6117,6 @@ is-string@^1.0.5: resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== -is-subset@^0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" - integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= - is-svg@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" @@ -7207,21 +6980,6 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= -lodash.escape@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" - integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= - -lodash.flattendeep@^4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" - integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= - -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= - lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -7576,11 +7334,6 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1: dependencies: minimist "^1.2.5" -moo@^0.5.0: - version "0.5.1" - resolved "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" - integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w== - move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -7663,16 +7416,6 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -nearley@^2.7.10: - version "2.20.1" - resolved "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" - integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== - dependencies: - commander "^2.19.0" - moo "^0.5.0" - railroad-diagrams "^1.0.0" - randexp "0.4.6" - negotiator@0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -7837,13 +7580,6 @@ nth-check@^1.0.2, nth-check@~1.0.1: dependencies: boolbase "~1.0.0" -nth-check@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" - integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== - dependencies: - boolbase "^1.0.0" - num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -7878,12 +7614,12 @@ object-hash@^2.0.1: resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== -object-inspect@^1.7.0, object-inspect@^1.8.0: +object-inspect@^1.8.0: version "1.9.0" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== -object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2: +object-is@^1.0.1: version "1.1.4" resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068" integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg== @@ -7908,7 +7644,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2: +object.assign@^4.1.0, object.assign@^4.1.1: version "4.1.2" resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -7918,7 +7654,7 @@ object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" -object.entries@^1.1.0, object.entries@^1.1.1, object.entries@^1.1.2: +object.entries@^1.1.0, object.entries@^1.1.1: version "1.1.3" resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6" integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg== @@ -7928,7 +7664,7 @@ object.entries@^1.1.0, object.entries@^1.1.1, object.entries@^1.1.2: es-abstract "^1.18.0-next.1" has "^1.0.3" -object.fromentries@^2.0.2, object.fromentries@^2.0.3: +object.fromentries@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072" integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw== @@ -8193,13 +7929,6 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse5-htmlparser2-tree-adapter@^6.0.0: - version "6.0.1" - resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" - integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== - dependencies: - parse5 "^6.0.1" - parse5@4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" @@ -8210,11 +7939,6 @@ parse5@5.1.0: resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== -parse5@^6.0.0, parse5@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - parseqs@0.0.6: version "0.0.6" resolved "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5" @@ -9202,15 +8926,6 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types-exact@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" - integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== - dependencies: - has "^1.0.3" - object.assign "^4.1.0" - reflect.ownkeys "^0.2.0" - prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -9335,19 +9050,6 @@ raf@^3.4.1: dependencies: performance-now "^2.1.0" -railroad-diagrams@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" - integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= - -randexp@0.4.6: - version "0.4.6" - resolved "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" - integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== - dependencies: - discontinuous-range "1.0.0" - ret "~0.1.10" - randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -9456,12 +9158,12 @@ react-i18next@^11.7.4: "@babel/runtime" "^7.3.1" html-parse-stringify2 "2.0.1" -react-is@^16.12.0, react-is@^16.13.1, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: +react-is@^16.12.0, react-is@^16.8.1, react-is@^16.8.4: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1: +"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1: version "17.0.1" resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== @@ -9562,6 +9264,14 @@ react-scripts@3.4.3: optionalDependencies: fsevents "2.1.2" +react-shallow-renderer@^16.13.1: + version "16.14.1" + resolved "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124" + integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg== + dependencies: + object-assign "^4.1.1" + react-is "^16.12.0 || ^17.0.0" + react-spinners@^0.9.0: version "0.9.0" resolved "https://registry.npmjs.org/react-spinners/-/react-spinners-0.9.0.tgz#b22c38acbfce580cd6f1b04a4649e812370b1fb8" @@ -9569,15 +9279,15 @@ react-spinners@^0.9.0: dependencies: "@emotion/core" "^10.0.15" -react-test-renderer@^16.0.0-0: - version "16.14.0" - resolved "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae" - integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg== +react-test-renderer@^17.0.1: + version "17.0.1" + resolved "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3187e636c3063e6ae498aedf21ecf972721574c7" + integrity sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA== dependencies: object-assign "^4.1.1" - prop-types "^15.6.2" - react-is "^16.8.6" - scheduler "^0.19.1" + react-is "^17.0.1" + react-shallow-renderer "^16.13.1" + scheduler "^0.20.1" react-transition-group@^2.9.0: version "2.9.0" @@ -9692,11 +9402,6 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -reflect.ownkeys@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" - integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= - regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -9995,14 +9700,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rst-selector-parser@^2.2.3: - version "2.2.3" - resolved "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" - integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE= - dependencies: - lodash.flattendeep "^4.4.0" - nearley "^2.7.10" - rsvp@^4.8.4: version "4.8.5" resolved "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" @@ -10100,6 +9797,14 @@ scheduler@^0.19.1: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.20.1: + version "0.20.1" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c" + integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -10135,7 +9840,7 @@ selfsigned@^1.10.7: dependencies: node-forge "^0.10.0" -"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -10679,15 +10384,6 @@ string.prototype.matchall@^4.0.2: regexp.prototype.flags "^1.3.0" side-channel "^1.0.3" -string.prototype.trim@^1.2.1: - version "1.2.3" - resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.3.tgz#d23a22fde01c1e6571a7fadcb9be11decd8061a7" - integrity sha512-16IL9pIBA5asNOSukPfxX2W68BaBvxyiRK16H3RA/lWW9BDosh+w7f+LhomPHpXJ82QEe7w7/rY/S1CV97raLg== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - string.prototype.trimend@^1.0.1: version "1.0.3" resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b" diff --git a/app/components/AllowConnectionForDeviceAlert.tsx b/app/components/AllowConnectionForDeviceAlert.tsx index 675399e..4e195e6 100644 --- a/app/components/AllowConnectionForDeviceAlert.tsx +++ b/app/components/AllowConnectionForDeviceAlert.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Intent, Alert, H4 } from '@blueprintjs/core'; -import isProduction from '../utils/isProduction'; import DeviceInfoCallout from './DeviceInfoCallout'; +import isWithReactRevealAnimations from '../utils/isWithReactRevealAnimations'; interface AllowConnectionForDeviceAlertProps { device: Device | null; @@ -25,7 +25,10 @@ export default function AllowConnectionForDeviceAlert( isOpen={isOpen} onCancel={onCancel} onConfirm={onConfirm} - transitionDuration={isProduction() ? 700 : 0} + transitionDuration={isWithReactRevealAnimations() ? 700 : 0} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + usePortal={false} >

Device is trying to connect, do you allow?

@@ -176,7 +177,11 @@ export default function ConnectedDevicesListDrawer( - +
{connectedDevicesService.getDevices().map((device) => { return ( @@ -245,7 +250,7 @@ export default function ConnectedDevicesListDrawer( setIsAlertDisconectAllOpen(false); }} onConfirm={handleDisconnectAndHideAllDevices} - transitionDuration={isProduction() ? 700 : 0} + transitionDuration={isWithReactRevealAnimations() ? 700 : 0} >

Are you sure you want to disconnect all connected viewing devices? diff --git a/app/components/SettingsOverlay/SettingsOverlay.tsx b/app/components/SettingsOverlay/SettingsOverlay.tsx index ef66054..3c3902a 100644 --- a/app/components/SettingsOverlay/SettingsOverlay.tsx +++ b/app/components/SettingsOverlay/SettingsOverlay.tsx @@ -29,8 +29,8 @@ import i18n_client, { getLangFullNameToLangISOKeyMap, getLangISOKeyToLangFullNameMap, } from '../../configs/i18next.config.client'; -import isProduction from '../../utils/isProduction'; import SettingRowLabelAndInput from './SettingRowLabelAndInput'; +import isWithReactRevealAnimations from '../../utils/isWithReactRevealAnimations'; const Fade = require('react-reveal/Fade'); @@ -259,7 +259,7 @@ export default function SettingsOverlay(props: SettingsOverlayProps) { transitionDuration={0} >
- +
- +
@@ -1569,6 +1583,7 @@ exports[`should match exact snapshot 1`] = ` > - // ) : ( - // <> - // ) - activeStep === 0 ? ( + // eslint-disable-next-line no-nested-ternary + process.env.NODE_ENV === 'production' && + process.env.RUN_MODE !== 'dev' && + process.env.RUN_MODE !== 'test' ? ( + <> + ) : activeStep === 0 ? ( + // eslint-disable-next-line react/jsx-indent + // ) : ( + // <> + // ) } { /**/ diff --git a/app/components/StepsOfStepper/ScanQRStep.tsx b/app/components/StepsOfStepper/ScanQRStep.tsx index 1118b70..6ac28ae 100644 --- a/app/components/StepsOfStepper/ScanQRStep.tsx +++ b/app/components/StepsOfStepper/ScanQRStep.tsx @@ -155,6 +155,7 @@ const ScanQRStep: React.FC = () => { canOutsideClickClose transitionDuration={isProduction() ? 700 : 0} style={{ position: 'relative', top: '0px' }} + usePortal={false} > - +
@@ -86,7 +89,7 @@ exports[`should match exact snapshot 1`] = `
- @@ -117,6 +120,7 @@ exports[`should match exact snapshot 1`] = ` >
diff --git a/app/components/StepsOfStepper/__snapshots__/ConfirmStep.spec.tsx.snap b/app/components/StepsOfStepper/__snapshots__/ConfirmStep.spec.tsx.snap index bc49e7d..c179c37 100644 --- a/app/components/StepsOfStepper/__snapshots__/ConfirmStep.spec.tsx.snap +++ b/app/components/StepsOfStepper/__snapshots__/ConfirmStep.spec.tsx.snap @@ -66,7 +66,10 @@ exports[`should match exact snapshot 1`] = ` } } > - +
@@ -130,7 +133,10 @@ exports[`should match exact snapshot 1`] = `
- +
@@ -141,7 +147,10 @@ exports[`should match exact snapshot 1`] = ` - + This should match with 'Device IP' displayed on the screen of device that is trying to connect. @@ -152,7 +161,10 @@ exports[`should match exact snapshot 1`] = ` } } > - + If IPs don't match click 'Deny' or 'Disconnect' button immediately to secure your computer! @@ -161,6 +173,7 @@ exports[`should match exact snapshot 1`] = ` } hoverCloseDelay={0} hoverOpenDelay={100} + minimal={false} position="top" transitionDuration={100} > @@ -171,7 +184,10 @@ exports[`should match exact snapshot 1`] = ` captureDismiss={false} content={ - + This should match with 'Device IP' displayed on the screen of device that is trying to connect. @@ -182,7 +198,10 @@ exports[`should match exact snapshot 1`] = ` } } > - + If IPs don't match click 'Deny' or 'Disconnect' button immediately to secure your computer! @@ -200,7 +219,13 @@ exports[`should match exact snapshot 1`] = ` interactionKind="hover-target" lazy={true} minimal={false} - modifiers={Object {}} + modifiers={ + Object { + "arrow": Object { + "enabled": true, + }, + } + } openOnTargetFocus={true} popoverClassName="bp3-tooltip" position="top" @@ -246,6 +271,8 @@ exports[`should match exact snapshot 1`] = ` >
- +
@@ -288,7 +318,10 @@ exports[`should match exact snapshot 1`] = `
- +
@@ -307,6 +340,7 @@ exports[`should match exact snapshot 1`] = `
- +
@@ -95,6 +98,8 @@ exports[`should match exact snapshot on each step 1`] = `
@@ -126,7 +132,13 @@ exports[`should match exact snapshot on each step 1`] = ` interactionKind="hover-target" lazy={true} minimal={false} - modifiers={Object {}} + modifiers={ + Object { + "arrow": Object { + "enabled": true, + }, + } + } openOnTargetFocus={true} popoverClassName="bp3-tooltip" position="left" @@ -166,6 +178,7 @@ exports[`should match exact snapshot on each step 1`] = `
@@ -956,6 +992,7 @@ exports[`should match exact snapshot on each step 2`] = ` >
@@ -33,6 +38,7 @@ exports[` when rendered should match exact snapshot 1`] = ` content="Click to make bigger" hoverCloseDelay={0} hoverOpenDelay={100} + minimal={false} position="left" transitionDuration={100} > @@ -69,6 +75,8 @@ exports[` when rendered should match exact snapshot 1`] = ` > or type the following address manualy in browser address bar on any device: @@ -77,6 +85,7 @@ exports[` when rendered should match exact snapshot 1`] = ` content="Click to copy" hoverCloseDelay={0} hoverOpenDelay={100} + minimal={false} position="left" transitionDuration={100} > @@ -107,6 +116,7 @@ exports[` when rendered should match exact snapshot 1`] = ` } } transitionDuration={0} + usePortal={false} > - +
@@ -139,6 +142,8 @@ exports[`should match exact snapshot 1`] = ` >

+ +
); }, [getClassesCallback]); @@ -200,11 +200,9 @@ export default function TopPanel(props: any) { {renderLogoWithAppName()}
- - {renderConnectedDevicesListButton()} - {renderHelpButton()} - {renderSettingsButton()} - + {renderConnectedDevicesListButton()} + {renderHelpButton()} + {renderSettingsButton()}
- -
-
-
-
-
-
- - - - feed - - - - -
-

- Device is trying to connect, do you allow? -

-

- Partner Device Info: -

-
-
-
-
- Device Type: - -
- - -
-
- Device IP: - -
-
-
-
-
- Device Browser: - -
-
- Device OS: - -
-
-
- Session ID: - -
-
-
-
-
-
-
- -
-
-
-
- - } + - + +
+ + + + +
-
-
feed -
+ +
+

Device is trying to connect, do you allow?

-

+ + - Partner Device Info: -

-
+ Partner Device Info: + + +
-
- Device Type: - -
- -
-
- Device IP: - -
-
-
-
-
- Device Browser: - -
-
- Device OS: - -
-
-
- Session ID: - -
-
-
-
-
-
-
- -
-
-
-
- } - > - -
- - -
- - - - -
-
-
- - - - - feed - - - - - -
- -

- Device is trying to connect, do you allow? -

-
- - -

- Partner Device Info: -

-
- -
- -
-
- -
+
+
+ + - Device Type: - -
- - - - This should match with 'Device IP' displayed on the screen of device + This should match with 'Device IP' displayed on the screen of device that is trying to connect. - - - - If IPs don't match click 'Deny' or 'Disconnect' button immediately to - secure your computer! - - - - } - hoverCloseDelay={0} - hoverOpenDelay={100} - position="top" - transitionDuration={100} - > - - - This should match with 'Device IP' displayed on the screen of device - that is trying to connect. - - - - If IPs don't match click 'Deny' or 'Disconnect' button immediately to - secure your computer! - - - + + - - + If IPs don't match click 'Deny' or 'Disconnect' button immediately to + secure your computer! + + + + } + hoverCloseDelay={0} + hoverOpenDelay={100} + minimal={false} + position="top" + transitionDuration={100} + > + + + This should match with 'Device IP' displayed on the screen of device + that is trying to connect. + + + - + + + } + defaultIsOpen={false} + disabled={false} + enforceFocus={false} + fill={false} + hasBackdrop={false} + hoverCloseDelay={0} + hoverOpenDelay={100} + inheritDarkTheme={true} + interactionKind="hover-target" + lazy={true} + minimal={false} + modifiers={ + Object { + "arrow": Object { + "enabled": true, + }, + } + } + openOnTargetFocus={true} + popoverClassName="bp3-tooltip" + position="top" + targetTagName="span" + transitionDuration={100} + usePortal={true} + wrapperTagName="span" + > + + + + + - - -
- -
- Device IP: - -
-
+ Device IP: +
-
-
-
-
- -
-
-
-
- -
- Device Browser: - -
-
- -
- Device OS: - -
-
-
- -
- Session ID: - -
-
-
+ +
+ + + + + + + + + + +
+ Device Browser: +
- +
+ +
+ Device OS: + +
+
+
+ +
+ Session ID: + +
+
+
- +
- - -
-
-
- - - - - - -
+ +
+ +
- - -
- - - +
+ + + + + + +
+
+
+ + +
+ diff --git a/app/components/__snapshots__/ConnectedDevicesListDrawer.spec.tsx.snap b/app/components/__snapshots__/ConnectedDevicesListDrawer.spec.tsx.snap index 0a3d87e..647fe18 100644 --- a/app/components/__snapshots__/ConnectedDevicesListDrawer.spec.tsx.snap +++ b/app/components/__snapshots__/ConnectedDevicesListDrawer.spec.tsx.snap @@ -311,6 +311,7 @@ exports[`should match exact snapshot 1`] = ` onKeyDown={[Function]} >
- @@ -55,6 +55,7 @@ exports[`should match exact snapshot 1`] = ` >
@@ -69,110 +74,104 @@ exports[` should match exact snapshot 1`] = ` - -
- -

- Deskreen -

-
-
-
+ Deskreen + + +
- -
- - - - - -
-
+ + +
+
+ - - - - - -
-
+ + +
+
+ - - - - - -
-
+ + + +
createStyles({ stepContent: { @@ -64,8 +59,6 @@ function getSteps() { const DeskreenStepper = React.forwardRef((_props, ref) => { const classes = useStyles(); - const [isInterShow, setIsInterShow] = useState(false); - const { isDarkTheme, currentLanguage } = useContext(SettingsContext); const { addToast } = useToasts(); @@ -97,17 +90,6 @@ const DeskreenStepper = React.forwardRef((_props, ref) => { setIsAlertOpen(true); } ); - - setTimeout( - () => { - setIsInterShow(true); - }, - isProduction() ? 500 : 0 - ); - - // sharingSessionService.setAppLanguage(currentLanguage); - // sharingSessionService.setAppTheme(isDarkTheme ? 'dark' : 'light'); - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { @@ -123,16 +105,7 @@ const DeskreenStepper = React.forwardRef((_props, ref) => { ] = useState(false); const steps = getSteps(); - const makeSmoothIntermediateStepTransition = () => { - if (!isProduction()) return; - setIsInterShow(false); - setTimeout(() => { - setIsInterShow(true); - }, 500); - }; - const handleNext = useCallback(() => { - makeSmoothIntermediateStepTransition(); if (activeStep === steps.length - 1) { setIsEntireScreenSelected(false); setIsApplicationWindowSelected(false); @@ -141,35 +114,24 @@ const DeskreenStepper = React.forwardRef((_props, ref) => { }, [activeStep, steps]); const handleNextEntireScreen = useCallback(() => { - makeSmoothIntermediateStepTransition(); setActiveStep((prevActiveStep) => prevActiveStep + 1); setIsEntireScreenSelected(true); }, []); const handleNextApplicationWindow = useCallback(() => { - makeSmoothIntermediateStepTransition(); setActiveStep((prevActiveStep) => prevActiveStep + 1); setIsApplicationWindowSelected(true); }, []); const handleBack = useCallback(() => { - makeSmoothIntermediateStepTransition(); setActiveStep((prevActiveStep) => prevActiveStep - 1); }, []); const handleReset = useCallback(() => { - makeSmoothIntermediateStepTransition(); setActiveStep(0); setPendingConnectionDevice(null); setIsUserAllowedConnection(false); - // const sharingSession = - // sharingSessionService.waitingForConnectionSharingSession; - // sharingSession?.disconnectByHostMachineUser(); - // sharingSession?.destory(); - // sharingSessionService.sharingSessions.delete(sharingSession?.id as string); - // sharingSessionService.waitingForConnectionSharingSession = null; - sharingSessionService .createWaitingForConnectionSharingSession() // eslint-disable-next-line promise/always-return @@ -184,7 +146,6 @@ const DeskreenStepper = React.forwardRef((_props, ref) => { }, []); const handleResetWithSharingSessionRestart = useCallback(() => { - makeSmoothIntermediateStepTransition(); setActiveStep(0); setPendingConnectionDevice(null); setIsUserAllowedConnection(false); @@ -275,39 +236,30 @@ const DeskreenStepper = React.forwardRef((_props, ref) => { const renderIntermediateOrSuccessStepContent = useCallback(() => { return activeStep === steps.length ? (
- - - - - + + +
) : (
- - setPendingConnectionDevice(null) - // eslint-disable-next-line react/jsx-curly-newline - } - resetUserAllowedConnection={() => setIsUserAllowedConnection(false)} - /> - + setPendingConnectionDevice(null) + // eslint-disable-next-line react/jsx-curly-newline + } + resetUserAllowedConnection={() => setIsUserAllowedConnection(false)} + />
); }, [ activeStep, steps, - isInterShow, handleReset, handleNext, handleBack, @@ -354,19 +306,17 @@ const DeskreenStepper = React.forwardRef((_props, ref) => { <> - - } - > - {steps.map((label, idx) => ( - {renderStepLabelContent(label, idx)} - ))} - - + } + > + {steps.map((label, idx) => ( + {renderStepLabelContent(label, idx)} + ))} + {renderIntermediateOrSuccessStepContent()} diff --git a/app/containers/HomePage.tsx b/app/containers/HomePage.tsx index 942c0cd..7e3ffe5 100644 --- a/app/containers/HomePage.tsx +++ b/app/containers/HomePage.tsx @@ -8,7 +8,7 @@ import { ToastProvider, DefaultToast } from 'react-toast-notifications'; import TopPanel from '../components/TopPanel'; import { LIGHT_UI_BACKGROUND } from './SettingsProvider'; -import DeskreenStepper from './Stepper'; +import DeskreenStepper from './DeskreenStepper'; // @ts-ignore: it is ok here, be like js it is fine // eslint-disable-next-line react/prop-types diff --git a/app/containers/__mocks__/electron-settings.ts b/app/containers/__mocks__/electron-settings.ts index 825710a..389c976 100644 --- a/app/containers/__mocks__/electron-settings.ts +++ b/app/containers/__mocks__/electron-settings.ts @@ -3,12 +3,12 @@ export default { if (name === 'appLanguage') { return true; } - return true; + return false; }, getSync: (name: string) => { if (name === 'appLanguage') { return 'en'; } - return 'en'; + return ''; }, }; diff --git a/app/containers/__snapshots__/DeskreenStepper.spec.tsx.snap b/app/containers/__snapshots__/DeskreenStepper.spec.tsx.snap new file mode 100644 index 0000000..e4b0839 --- /dev/null +++ b/app/containers/__snapshots__/DeskreenStepper.spec.tsx.snap @@ -0,0 +1,1604 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should match exact snapshot 1`] = ` + + + + + +
+ +
+ } + style={ + Object { + "background": "transparent", + } + } + > + } + style={ + Object { + "background": "transparent", + } + } + > + + +
+ + } + disabled={false} + index={0} + key=".$Connect" + last={false} + orientation="horizontal" + > + + } + disabled={false} + index={0} + last={false} + orientation="horizontal" + > +
+ + + + + +
+ + + + + feed + + + + + +
+
+
+ + + + + +
+ Connect +
+
+
+
+
+
+
+
+
+
+
+
+ + } + disabled={true} + index={1} + key=".$Select" + last={false} + orientation="horizontal" + > + + } + disabled={true} + index={1} + last={false} + orientation="horizontal" + > +
+ + + +
+ +
+
+
+
+ + + + + +
+ + + + + flow-branch + + + + + +
+
+
+ + + + + +
+ Select +
+
+
+
+
+
+
+
+
+
+
+
+ + } + disabled={true} + index={2} + key=".$Confirm" + last={true} + orientation="horizontal" + > + + } + disabled={true} + index={2} + last={true} + orientation="horizontal" + > +
+ + + +
+ +
+
+
+
+ + + + + +
+ + + + + confirm + + + + + +
+
+
+ + + + + +
+ Confirm +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ + +
+ +
+ +
+ + make sure your computer and device are connected to same WiFi + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ +
+ or type the following address manualy in browser address bar on any device: +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + +
+
+ +
+ + + + + + + + + + + + +
+ + } + > + + +
+ +
+
+
+ + +`; diff --git a/app/containers/__snapshots__/HomePage.spec.tsx.snap b/app/containers/__snapshots__/HomePage.spec.tsx.snap index 5ec838a..0c0ead7 100644 --- a/app/containers/__snapshots__/HomePage.spec.tsx.snap +++ b/app/containers/__snapshots__/HomePage.spec.tsx.snap @@ -86,6 +86,7 @@ exports[`should match exact snapshot 1`] = ` content="If you like Deskreen, consider donating! Deskreen is free and opensource forever! You can help us to make Deskreen even better!" hoverCloseDelay={0} hoverOpenDelay={100} + minimal={false} position="bottom" transitionDuration={100} > @@ -106,7 +107,13 @@ exports[`should match exact snapshot 1`] = ` interactionKind="hover-target" lazy={true} minimal={false} - modifiers={Object {}} + modifiers={ + Object { + "arrow": Object { + "enabled": true, + }, + } + } openOnTargetFocus={true} popoverClassName="bp3-tooltip" position="bottom" @@ -141,6 +148,7 @@ exports[`should match exact snapshot 1`] = ` key=".0" style={ Object { + "borderRadius": "100px", "marginRight": "10px", "transform": "translateY(2px)", } @@ -149,10 +157,12 @@ exports[`should match exact snapshot 1`] = ` >
@@ -391,538 +369,443 @@ exports[`should match exact snapshot 1`] = `
- - -
- - + - - + - - - - - + - - - - - - - - - - - -
-
- + + + + + + + + + + + + + +
+
+ -
- - + - - + - - - - - + - - - - - - - - - - - -
- - + + + + + + + + + + + + +
+
+
+ -
- - + - - + - - - - - + - - - - - - - - - - - -
- - + + + + + + + + + + + + + +
+
- } + style={ + Object { + "background": "transparent", + } + } > - } + style={ + Object { + "background": "transparent", } } - outEffect={false} - refProp="ref" - top={true} > -
- } + - } +
- } + disabled={false} + index={0} + key=".$Connect" + last={false} + orientation="horizontal" > - } + disabled={false} + index={0} + last={false} + orientation="horizontal" >
- - } disabled={false} - index={0} - key=".$Connect" + expanded={false} + icon={1} + id="step-label-deskreen" + key=".0" last={false} orientation="horizontal" > - - } disabled={false} - index={0} + expanded={false} + icon={1} + id="step-label-deskreen" last={false} orientation="horizontal" > -
- - - + + + + + feed + + + + + +
+ + + + + -
- - - - - feed - - - - - + Connect
-
+
- - - - - -
- Connect -
-
-
-
-
-
-
- - -
- - - + + + + + +
+ + + + } + disabled={true} + index={1} + key=".$Select" + last={false} + orientation="horizontal" + > + + } + disabled={true} + index={1} + last={false} + orientation="horizontal" + > +
+ - } disabled={true} index={1} - key=".$Select" - last={false} orientation="horizontal" > - - } disabled={true} index={1} + orientation="horizontal" + > + +
+ +
+
+ +
+ + -
- - - -
-
-
-
-
- + + + flow-branch + + + + + +
+ + + - - -
- - - - - flow-branch - - - - - + Select
-
+
- - - - - -
- Select -
-
-
-
-
-
-
-
-
-
-
-
- + + + + + +
+ + + + } + disabled={true} + index={2} + key=".$Confirm" + last={true} + orientation="horizontal" + > + + } + disabled={true} + index={2} + last={true} + orientation="horizontal" + > +
+ - } disabled={true} index={2} - key=".$Confirm" + orientation="horizontal" + > + + +
+ +
+
+
+
+ - - } disabled={true} - index={2} + expanded={false} + icon={3} + id="step-label-deskreen" last={true} orientation="horizontal" > -
- - - -
-
-
-
-
- + + + confirm + + + + + +
+ + + - - -
- - - - - confirm - - - - - + Confirm
-
+
- - - - - -
- Confirm -
-
-
-
-
-
-
-
-
-
-
-
+ + + + + +
- - - - - - - + + + + + + + - -
- - +
-
- -
+ - -
- - make sure your computer and device are connected to same WiFi - -
-
- -
- -
-
- - - - - - - - - - - - - - - - - - - - -
-
+
+
+ +
+ +
+
+ + - + + -
- or type the following address manualy in browser address bar on any device: -
-
-
- - - - - - - - - + - - - - - - - - - - - - + + + + + +
+ + + + + + + + + + + + +
+
+ +
+ or type the following address manualy in browser address bar on any device: +
+
+
+ + + + + + + + + + + + + + + - -
- + + + + + + + + + + + +
+ + - - -
- - - -
-
-
- - + + + + + No, I need to share other thing + + + +
+
+
- - + +
@@ -2571,6 +2398,7 @@ exports[`should match exact snapshot 1`] = ` onCancel={[Function]} onConfirm={[Function]} transitionDuration={0} + usePortal={false} > diff --git a/app/containers/__snapshots__/Stepper.spec.tsx.snap b/app/containers/__snapshots__/Stepper.spec.tsx.snap deleted file mode 100644 index 819e978..0000000 --- a/app/containers/__snapshots__/Stepper.spec.tsx.snap +++ /dev/null @@ -1,1658 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should match exact snapshot 1`] = ` - - - - - -
- -
- - -
- } - style={ - Object { - "background": "transparent", - } - } - > - } - style={ - Object { - "background": "transparent", - } - } - > - - -
- - } - disabled={false} - index={0} - key=".$Connect" - last={false} - orientation="horizontal" - > - - } - disabled={false} - index={0} - last={false} - orientation="horizontal" - > -
- - - - - -
- - - - - feed - - - - - -
-
-
- - - - - -
- Connect -
-
-
-
-
-
-
-
-
-
-
-
- - } - disabled={true} - index={1} - key=".$Select" - last={false} - orientation="horizontal" - > - - } - disabled={true} - index={1} - last={false} - orientation="horizontal" - > -
- - - -
- -
-
-
-
- - - - - -
- - - - - flow-branch - - - - - -
-
-
- - - - - -
- Select -
-
-
-
-
-
-
-
-
-
-
-
- - } - disabled={true} - index={2} - key=".$Confirm" - last={true} - orientation="horizontal" - > - - } - disabled={true} - index={2} - last={true} - orientation="horizontal" - > -
- - - -
- -
-
-
-
- - - - - -
- - - - - confirm - - - - - -
-
-
- - - - - -
- Confirm -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
-
- - -
- - -
- -
- -
- - make sure your computer and device are connected to same WiFi - -
-
- -
- -
-
- - - - - - - - - - - - - - - - - - - - -
-
- -
- or type the following address manualy in browser address bar on any device: -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
-
-
- - -
- - -
-
- -
- - - - - - - - - - - - -
- - } - > - - -
- -
-
-
- - -`; diff --git a/app/features/DesktopCapturerSourcesService/index.ts b/app/features/DesktopCapturerSourcesService/index.ts index 4e126b3..be1d941 100644 --- a/app/features/DesktopCapturerSourcesService/index.ts +++ b/app/features/DesktopCapturerSourcesService/index.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable class-methods-use-this */ import { desktopCapturer, DesktopCapturerSource } from 'electron'; -import Logger from '../../utils/logger'; +import Logger from '../../utils/LoggerWithFilePrefix'; import DesktopCapturerSourceType from './DesktopCapturerSourceType'; const log = new Logger(__filename); diff --git a/app/features/PeerConnection/index.ts b/app/features/PeerConnection/index.ts index ab6a875..7b43cc6 100644 --- a/app/features/PeerConnection/index.ts +++ b/app/features/PeerConnection/index.ts @@ -14,7 +14,7 @@ import SharingSessionStatusEnum from '../SharingSessionsService/SharingSessionSt import RoomIDService from '../../server/RoomIDService'; import SharingSessionsService from '../SharingSessionsService'; import connectSocket from '../../server/connectSocket'; -import Logger from '../../utils/logger'; +import Logger from '../../utils/LoggerWithFilePrefix'; import DesktopCapturerSources from '../DesktopCapturerSourcesService'; import setSdpMediaBitrate from './setSdpMediaBitrate'; import getDesktopSourceStreamBySourceID from './getDesktopSourceStreamBySourceID'; @@ -150,8 +150,9 @@ export default class PeerConnection { } return size; }) - .then(() => { - this.createPeer(); + .then(async () => { + await this.createPeer(); + this.setDisplaySizeFromLocalStream(); return undefined; }); } @@ -164,6 +165,20 @@ export default class PeerConnection { this.onDeviceConnectedCallback = callback; } + setDisplaySizeFromLocalStream() { + if (!this.localStream || !this.localStream.getVideoTracks()[0]) return; + if (!this.localStream.getVideoTracks()[0].getSettings().width) return; + if (!this.localStream.getVideoTracks()[0].getSettings().height) return; + this.sourceDisplaySize = { + width: this.localStream.getVideoTracks()[0].getSettings().width + ? (this.localStream.getVideoTracks()[0].getSettings().width as number) + : 640, + height: this.localStream.getVideoTracks()[0].getSettings().height + ? (this.localStream.getVideoTracks()[0].getSettings().height as number) + : 480, + }; + } + denyConnectionForPartner() { this.sendEncryptedMessage({ type: 'DENY_TO_CONNECT', @@ -208,7 +223,7 @@ export default class PeerConnection { this.partnerDeviceDetails = {} as Device; } - private initSocketWhenUserCreatedCallback() { + initSocketWhenUserCreatedCallback() { this.socket.removeAllListeners(); this.socket.on('disconnect', () => { @@ -282,7 +297,7 @@ export default class PeerConnection { }); } - private selfDestrory() { + selfDestrory() { this.partner = nullUser; this.connectedDevicesService.removeDeviceByID(this.partnerDeviceDetails.id); if (this.peer !== nullSimplePeer) { @@ -306,7 +321,7 @@ export default class PeerConnection { this.roomIDService.unmarkRoomIDAsTaken(this.roomID); } - private emitUserEnter() { + emitUserEnter() { if (!this.socket) return; this.socket.emit('USER_ENTER', { username: this.user.username, @@ -378,70 +393,79 @@ export default class PeerConnection { } createPeer() { - this.createDesktopCapturerStream(this.desktopCapturerSourceID).then(() => { - const peer = new SimplePeer({ - initiator: true, - // trickle: true, - // stream: this.localStream, - // allowHalfTrickle: false, - config: { iceServers: [] }, - sdpTransform: (sdp) => { - let newSDP = sdp; - newSDP = setSdpMediaBitrate( - newSDP as string, - 'video', - 500000 - ) as typeof sdp; - return newSDP; - }, - }); + return new Promise((resolve) => { + this.createDesktopCapturerStream(this.desktopCapturerSourceID).then( + () => { + const peer = new SimplePeer({ + initiator: true, + // trickle: true, + // stream: this.localStream, + // allowHalfTrickle: false, + config: { iceServers: [] }, + sdpTransform: (sdp) => { + let newSDP = sdp; + newSDP = setSdpMediaBitrate( + newSDP as string, + 'video', + 500000 + ) as typeof sdp; + return newSDP; + }, + }); - if (this.localStream !== null) { - peer.addStream(this.localStream); - } - - peer.on('signal', (data: string) => { - // fired when simple peer and webrtc done preparation to start call on this machine - this.signalsDataToCallUser.push(data); - }); - - this.peer = peer; - - this.peer.on('data', async (data) => { - const dataJSON = JSON.parse(data); - - if (dataJSON.type === 'set_video_quality') { - const maxVideoQualityMultiplier = dataJSON.payload.value; - const minVideoQualityMultiplier = - maxVideoQualityMultiplier === 1 ? 0.5 : maxVideoQualityMultiplier; - - if (!this.desktopCapturerSourceID.includes('screen')) return; - - const newStream = await getDesktopSourceStreamBySourceID( - this.desktopCapturerSourceID, - this.sourceDisplaySize?.width, - this.sourceDisplaySize?.height, - minVideoQualityMultiplier, - maxVideoQualityMultiplier - ); - const newVideoTrack = newStream.getVideoTracks()[0]; - const oldTrack = this.localStream?.getVideoTracks()[0]; - - if (oldTrack && this.localStream) { - peer.replaceTrack(oldTrack, newVideoTrack, this.localStream); - oldTrack.stop(); + // eslint-disable-next-line promise/always-return + if (this.localStream !== null) { + peer.addStream(this.localStream); } - } - if (dataJSON.type === 'get_sharing_source_type') { - const sourceType = this.desktopCapturerSourceID.includes('screen') - ? 'screen' - : 'window'; + peer.on('signal', (data: string) => { + // fired when simple peer and webrtc done preparation to start call on this machine + this.signalsDataToCallUser.push(data); + }); - this.peer.send(prepareDataMessageToSendScreenSourceType(sourceType)); + this.peer = peer; + + this.peer.on('data', async (data) => { + const dataJSON = JSON.parse(data); + + if (dataJSON.type === 'set_video_quality') { + const maxVideoQualityMultiplier = dataJSON.payload.value; + const minVideoQualityMultiplier = + maxVideoQualityMultiplier === 1 + ? 0.5 + : maxVideoQualityMultiplier; + + if (!this.desktopCapturerSourceID.includes('screen')) return; + + const newStream = await getDesktopSourceStreamBySourceID( + this.desktopCapturerSourceID, + this.sourceDisplaySize?.width, + this.sourceDisplaySize?.height, + minVideoQualityMultiplier, + maxVideoQualityMultiplier + ); + const newVideoTrack = newStream.getVideoTracks()[0]; + const oldTrack = this.localStream?.getVideoTracks()[0]; + + if (oldTrack && this.localStream) { + peer.replaceTrack(oldTrack, newVideoTrack, this.localStream); + oldTrack.stop(); + } + } + + if (dataJSON.type === 'get_sharing_source_type') { + const sourceType = this.desktopCapturerSourceID.includes('screen') + ? 'screen' + : 'window'; + + this.peer.send( + prepareDataMessageToSendScreenSourceType(sourceType) + ); + } + }); + resolve(undefined); } - }); - return peer; + ); }); } @@ -449,12 +473,12 @@ export default class PeerConnection { createDesktopCapturerStream(sourceID: string) { return new Promise((resolve) => { try { - if (process.env.RUN_MODE === 'test') resolve(); + if (process.env.RUN_MODE === 'test') resolve(undefined); if (!sourceID.includes('screen')) { getDesktopSourceStreamBySourceID(sourceID).then((stream) => { this.localStream = stream; - resolve(); + resolve(undefined); return stream; }); } else { @@ -467,7 +491,7 @@ export default class PeerConnection { 1 ).then((stream) => { this.localStream = stream; - resolve(); + resolve(undefined); return stream; }); } diff --git a/app/features/PeerConnectionHelperRendererService/index.ts b/app/features/PeerConnectionHelperRendererService/index.ts index caabc39..60b2929 100644 --- a/app/features/PeerConnectionHelperRendererService/index.ts +++ b/app/features/PeerConnectionHelperRendererService/index.ts @@ -18,8 +18,8 @@ export default class RendererWebrtcHelpersService { helperRendererWindow = new BrowserWindow({ show: false, - width: 300, - height: 300, + // width: 300, + // height: 300, // x: 2147483647, // y: 2147483647, // transparent: true, @@ -54,6 +54,7 @@ export default class RendererWebrtcHelpersService { if (!helperRendererWindow) { throw new Error('"helperRendererWindow" is not defined'); } + helperRendererWindow.webContents.send('start-peer-connection'); }); helperRendererWindow.on('closed', () => { diff --git a/app/features/SharingSessionsService/SharingSession.ts b/app/features/SharingSessionsService/SharingSession.ts index 4fdfc30..ce8a56d 100644 --- a/app/features/SharingSessionsService/SharingSession.ts +++ b/app/features/SharingSessionsService/SharingSession.ts @@ -134,7 +134,7 @@ export default class SharingSession { for (let i = 0; i < this.statusChangeListeners.length; i += 1) { this.statusChangeListeners[i](this.id); } - resolve(); + resolve(undefined); }); } diff --git a/app/features/counter/__snapshots__/Counter.spec.tsx.snap b/app/features/counter/__snapshots__/Counter.spec.tsx.snap index 9a35003..2334b9b 100644 --- a/app/features/counter/__snapshots__/Counter.spec.tsx.snap +++ b/app/features/counter/__snapshots__/Counter.spec.tsx.snap @@ -15,6 +15,7 @@ exports[`Counter component should match exact snapshot 1`] = ` >