├── .codecov.yml ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-Bug_report.md │ └── 3-Feature_request.md ├── config.yml ├── stale.yml └── workflows │ ├── build-and-test.yml │ ├── codecov.yml │ ├── post-release-publish.yml │ ├── publish-to-wiki.yml │ └── release.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app ├── .testcafe-electron-rc ├── Routes.tsx ├── __snapshots__ │ └── menu.spec.ts.snap ├── api │ ├── config.ts │ └── urlGenerator.ts ├── app.global.css ├── app.html ├── app.icns ├── client │ ├── .codecov.yml │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── locales │ │ │ ├── da │ │ │ │ └── translation.json │ │ │ ├── de │ │ │ │ └── translation.json │ │ │ ├── en │ │ │ │ └── translation.json │ │ │ ├── es │ │ │ │ └── translation.json │ │ │ ├── fi │ │ │ │ └── translation.json │ │ │ ├── fr │ │ │ │ └── translation.json │ │ │ ├── it │ │ │ │ └── translation.json │ │ │ ├── ja │ │ │ │ └── translation.json │ │ │ ├── ko │ │ │ │ └── translation.json │ │ │ ├── nl │ │ │ │ └── translation.json │ │ │ ├── ru │ │ │ │ └── translation.json │ │ │ ├── sv │ │ │ │ └── translation.json │ │ │ ├── ua │ │ │ │ └── translation.json │ │ │ ├── zh_CN │ │ │ │ └── translation.json │ │ │ └── zh_TW │ │ │ │ └── translation.json │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── sonar-project.properties │ ├── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── api │ │ │ ├── config.ts │ │ │ ├── generator.spec.ts │ │ │ ├── generator.ts │ │ │ └── mocks │ │ │ │ └── generatorTestVariables.ts │ │ ├── components │ │ │ ├── ConnectingIndicator │ │ │ │ ├── ConnectingIndicatorIcon.tsx │ │ │ │ ├── LoadingSharingIcon.tsx │ │ │ │ └── index.tsx │ │ │ ├── ErrorDialog │ │ │ │ ├── ErrorMessageEnum.ts │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── MyDeviceInfoCard │ │ │ │ ├── DeviceDetails.d.ts │ │ │ │ └── index.tsx │ │ │ ├── PlayerControlPanel │ │ │ │ ├── handlePlayerToggleFullscreen.ts │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ └── initScreenfullOnChange.ts │ │ │ └── ToggleDarkModeSwitch │ │ │ │ └── index.tsx │ │ ├── config │ │ │ └── i18n.ts │ │ ├── constants │ │ │ ├── appConstants.ts │ │ │ └── styleConstants.ts │ │ ├── containers │ │ │ ├── ConnectionPrompts │ │ │ │ └── index.tsx │ │ │ ├── MainView │ │ │ │ ├── ConnectionIconEnum.ts │ │ │ │ ├── LoadingSharingIconEnum.ts │ │ │ │ ├── changeLanguage.ts │ │ │ │ ├── handleCreatePeerConnection.ts │ │ │ │ ├── handleDisplayingLoadingSharingIconLoop.spec.ts │ │ │ │ ├── handleDisplayingLoadingSharingIconLoop.ts │ │ │ │ ├── handleNoConnectionTimeout.ts │ │ │ │ ├── handleRemoveDanglingReactRevealContainer.ts │ │ │ │ ├── handleSetVideoQuality.ts │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ └── PlayerView │ │ │ │ └── index.tsx │ │ ├── features │ │ │ ├── PeerConnection │ │ │ │ ├── NullUser.ts │ │ │ │ ├── PartnerPeerUser.d.ts │ │ │ │ ├── PeerConnection.d.ts │ │ │ │ ├── PeerConnectionUIHandler.ts │ │ │ │ ├── ReceiveEncryptedMessagePayload.d.ts │ │ │ │ ├── ScreenSharingSourceEnum.ts │ │ │ │ ├── errors │ │ │ │ │ ├── PeerConnectionPartnerIsNotDefinedError.ts │ │ │ │ │ ├── PeerConnectionPeerIsNullError.ts │ │ │ │ │ ├── PeerConnectionSocketNotDefined.ts │ │ │ │ │ └── PeerConnectionUserIsNotDefinedError.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ ├── mocks │ │ │ │ │ ├── INPUTtestWindowNavigatorUserAgent.ts │ │ │ │ │ ├── INPUTvideo500000testSdpMediaBitrate.ts │ │ │ │ │ ├── OUTPUTDeviceDetailsFromUAParsed.ts │ │ │ │ │ └── OUTPUTvideo500000testSdpMediaBitrate.ts │ │ │ │ ├── peerConnectionHandlePeer.spec.ts │ │ │ │ ├── peerConnectionHandlePeer.ts │ │ │ │ ├── peerConnectionHandleSocket.spec.ts │ │ │ │ ├── peerConnectionHandleSocket.ts │ │ │ │ ├── peerConnectionReceiveEncryptedMessage.spec.ts │ │ │ │ ├── peerConnectionReceiveEncryptedMessage.ts │ │ │ │ ├── setAndShowErrorDialogMessage.spec.ts │ │ │ │ ├── setAndShowErrorDialogMessage.ts │ │ │ │ ├── setSdpMediaBitrate.spec.ts │ │ │ │ ├── setSdpMediaBitrate.ts │ │ │ │ ├── simplePeerDataMessages.ts │ │ │ │ └── startSocketConnectedCheckingLoop │ │ │ │ │ ├── index.spec.ts │ │ │ │ │ └── index.ts │ │ │ └── VideoAutoQualityOptimizer │ │ │ │ ├── VideoQualityEnum.ts │ │ │ │ ├── errors │ │ │ │ ├── CanvasNotDefinedError.ts │ │ │ │ ├── ImageDataIsUndefinedError.ts │ │ │ │ ├── VideoDimensionsAreWrongError.ts │ │ │ │ └── VideoNotDefinedError.ts │ │ │ │ ├── index.spec.ts │ │ │ │ └── index.ts │ │ ├── images │ │ │ ├── deskreen_logo_128x128.png │ │ │ ├── fullscreen_24px.svg │ │ │ ├── fullscreen_exit-24px.svg │ │ │ └── red_heart_2764_twemoji_120x120.png │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── polyfills.tsx │ │ ├── providers │ │ │ └── AppContextProvider │ │ │ │ └── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── serviceWorker.ts │ │ ├── setupTests.ts │ │ └── utils │ │ │ ├── ProcessedMessage.d.ts │ │ │ ├── areWeTestingWithJest.ts │ │ │ ├── crypto.ts │ │ │ ├── getRoomIDOfCurrentBrowserWindow.ts │ │ │ ├── message.spec.ts │ │ │ ├── message.ts │ │ │ ├── mocks │ │ │ ├── getTestPrivateKeyPem.ts │ │ │ └── getTestPublicKeyPem.ts │ │ │ ├── socket.ts │ │ │ └── userAgentParserHelpers.ts │ ├── tsconfig.json │ └── yarn.lock ├── components │ ├── AllowConnectionForDeviceAlert.tsx │ ├── CloseOverlayButton.tsx │ ├── ConnectedDevicesListDrawer.tsx │ ├── DeviceInfoCallout │ │ └── index.tsx │ ├── LanguageSelector │ │ └── index.tsx │ ├── SettingsOverlay │ │ ├── SettingRowLabelAndInput.tsx │ │ └── SettingsOverlay.tsx │ ├── ShareAppOrScreenControlGroup.tsx │ ├── SharingSourcePreviewCard │ │ └── index.tsx │ ├── StepperPanel │ │ ├── ColorlibConnector.tsx │ │ ├── ColorlibStepIcon.tsx │ │ └── DeviceConnectedInfoButton.tsx │ ├── StepsOfStepper │ │ ├── ChooseAppOrScreeenStep.tsx │ │ ├── ChooseAppOrScreenOverlay │ │ │ ├── ChooseAppOrScreenOverlay.tsx │ │ │ ├── PreviewGridList.tsx │ │ │ └── ViewSharingObject.d.ts │ │ ├── ConfirmStep.tsx │ │ ├── IntermediateStep.spec.tsx │ │ ├── IntermediateStep.tsx │ │ ├── ScanQRStep.spec.tsx │ │ ├── ScanQRStep.tsx │ │ ├── SuccessStep.tsx │ │ └── __snapshots__ │ │ │ ├── IntermediateStep.spec.tsx.snap │ │ │ └── ScanQRStep.spec.tsx.snap │ ├── ToggleThemeBtnGroup │ │ └── index.tsx │ ├── TopPanel.spec.tsx │ ├── TopPanel.tsx │ └── css.d.ts ├── configs │ ├── app.lang.config.ts │ ├── i18next.config.client.spec.ts │ ├── i18next.config.client.ts │ └── i18next.config.ts ├── constants │ ├── routes.json │ └── test-devices.json ├── containers │ ├── App.tsx │ ├── DeskreenStepper.tsx │ ├── HomePage.tsx │ ├── Root.tsx │ ├── SettingsProvider.tsx │ └── __mocks__ │ │ ├── electron-settings.ts │ │ ├── electron.ts │ │ └── react-i18next.ts ├── deskreen-electron-store.ts ├── enums │ └── ElectronStoreKeys.enum.ts ├── features │ ├── ConnectedDevicesService │ │ ├── Device.d.ts │ │ ├── index.spec.ts │ │ └── index.ts │ ├── DesktopCapturerSourcesService │ │ ├── DesktopCapturerSourceType.ts │ │ ├── DesktopCapturerSourceWithType.d.ts │ │ ├── index.spec.ts │ │ └── index.ts │ ├── PeerConnection │ │ ├── NullSimplePeer.ts │ │ ├── NullUser.ts │ │ ├── PartnerPeerUser.d.ts │ │ ├── PeerConnection.d.ts │ │ ├── ReceiveEncryptedMessagePayload.d.ts │ │ ├── SendEncryptedMessagePayload.d.ts │ │ ├── createDesktopCapturerStream.spec.ts │ │ ├── createDesktopCapturerStream.ts │ │ ├── getDesktopSourceStreamBySourceID.spec.ts │ │ ├── getDesktopSourceStreamBySourceID.ts │ │ ├── handleCreatePeer.spec.ts │ │ ├── handleCreatePeer.ts │ │ ├── handlePeerOnData.spec.ts │ │ ├── handlePeerOnData.ts │ │ ├── handleRecieveEncryptedMessage.spec.ts │ │ ├── handleRecieveEncryptedMessage.ts │ │ ├── handleSelfDestroy.spec.ts │ │ ├── handleSelfDestroy.ts │ │ ├── handleSetDisplaySizeFromLocalStream.spec.ts │ │ ├── handleSetDisplaySizeFromLocalStream.ts │ │ ├── handleSocket.spec.ts │ │ ├── handleSocket.ts │ │ ├── handleSocketUserEnter.spec.ts │ │ ├── handleSocketUserEnter.ts │ │ ├── handleSocketUserExit.spec.ts │ │ ├── handleSocketUserExit.ts │ │ ├── index.spec.ts │ │ ├── index.ts │ │ ├── mocks │ │ │ ├── INPUTvideo500000testSdpMediaBitrate.ts │ │ │ ├── OUTPUTvideo500000testSdpMediaBitrate.ts │ │ │ └── testVars.ts │ │ ├── prepareDataMessageToSendScreenSourceType.ts │ │ ├── setSdpMediaBitrate.spec.ts │ │ ├── setSdpMediaBitrate.ts │ │ ├── simplePeerHandleSdpTransform.spec.ts │ │ └── simplePeerHandleSdpTransform.ts │ ├── PeerConnectionHelperRendererService │ │ ├── index.spec.ts │ │ └── index.ts │ └── SharingSessionService │ │ ├── LocalPeerUser.d.ts │ │ ├── SharingSession.spec.ts │ │ ├── SharingSession.ts │ │ ├── SharingSessionStatusChangeListener.d.ts │ │ ├── SharingSessionStatusEnum.ts │ │ ├── SharingTypeEnum.ts │ │ ├── __mocks__ │ │ ├── PeerConnection.ts │ │ └── shortid.ts │ │ ├── index.spec.ts │ │ └── index.ts ├── images │ └── red_heart_2764_twemoji_120x120.png ├── index.tsx ├── locales │ ├── da │ │ └── translation.json │ ├── de │ │ └── translation.json │ ├── en │ │ └── translation.json │ ├── es │ │ └── translation.json │ ├── fi │ │ └── translation.json │ ├── fr │ │ └── translation.json │ ├── it │ │ └── translation.json │ ├── ja │ │ └── translation.json │ ├── ko │ │ └── translation.json │ ├── nl │ │ └── translation.json │ ├── ru │ │ └── translation.json │ ├── sv │ │ └── translation.json │ ├── ua │ │ └── translation.json │ ├── zh_CN │ │ └── translation.json │ └── zh_TW │ │ └── translation.json ├── main.dev.spec.ts ├── main.dev.ts ├── main.prod.js.LICENSE ├── main.prod.js.LICENSE.txt ├── main │ ├── IpcEvents.enum.ts │ └── ipcMainHandlers.ts ├── menu.spec.ts ├── menu.ts ├── package-lock.json ├── package.json ├── peerConnectionHelperRendererWindow.html ├── peerConnectionHelperRendererWindowIndex.spec.ts ├── peerConnectionHelperRendererWindowIndex.tsx ├── rootReducer.ts ├── server │ ├── Room.d.ts │ ├── RoomIDService │ │ ├── RoomIDService.spec.ts │ │ └── index.ts │ ├── connectSocket.ts │ ├── darkwireSocket.spec.ts │ ├── darkwireSocket.ts │ ├── index.ts │ ├── pollForInactiveRooms.spec.ts │ ├── pollForInactiveRooms.ts │ ├── socketsIPService.ts │ └── store │ │ ├── MemoryStore.ts │ │ ├── index.ts │ │ └── socketIOServerStore.ts ├── store.ts ├── utils │ ├── AppUpdater.ts │ ├── LoggerWithFilePrefix.spec.ts │ ├── LoggerWithFilePrefix.ts │ ├── ProcessedMessage.d.ts │ ├── crypto.ts │ ├── getAppLanguage.ts │ ├── getAppTheme.ts │ ├── getNewVersionTag.ts │ ├── installExtensions.ts │ ├── isProduction.ts │ ├── isWithReactRevealAnimations.ts │ ├── mainProcessHelpers │ │ ├── DeskreenGlobal.d.ts │ │ ├── getDeskreenGlobal.ts │ │ └── initGlobals.ts │ ├── message.spec.ts │ ├── message.ts │ └── mocks │ │ ├── getTestPrivateKeyPem.ts │ │ └── getTestPublicKeyPem.ts └── yarn.lock ├── babel.config.js ├── configs ├── .eslintrc ├── webpack.config.base.js ├── webpack.config.eslint.js ├── webpack.config.main.prod.babel.js ├── webpack.config.renderer.dev.babel.js ├── webpack.config.renderer.dev.dll.babel.js └── webpack.config.renderer.prod.babel.js ├── doc ├── architecture │ ├── deskreen-arch-pavlobu-21012021.html │ └── deskreen-arch-pavlobu-21012021.svg ├── benchmarks │ └── 1-IPad-pro-2017-12.9-inch+Macbook-Pro-mid-2015.md ├── init-sharing-session │ ├── deskreen-webrtc-screen-sharing-session-initiation-pavlobu-22012021.html │ └── deskreen-webrtc-screen-sharing-session-initiation-pavlobu-22012021.svg └── translations-docs │ ├── fix-a-typo-in-existing-translation-of-Deskreen.md │ └── how-to-add-new-language-translation-to-Deskreen.md ├── drivers ├── README.md ├── linux │ └── git-dummy ├── mac │ └── git-dummy └── win │ └── git-dummy ├── internals ├── img │ ├── erb-banner.png │ ├── erb-logo.png │ ├── eslint-padded-90.png │ ├── eslint-padded.png │ ├── eslint.png │ ├── jest-padded-90.png │ ├── jest-padded.png │ ├── jest.png │ ├── js-padded.png │ ├── js.png │ ├── npm.png │ ├── react-padded-90.png │ ├── react-padded.png │ ├── react-router-padded-90.png │ ├── react-router-padded.png │ ├── react-router.png │ ├── react.png │ ├── redux-padded-90.png │ ├── redux-padded.png │ ├── redux.png │ ├── webpack-padded-90.png │ ├── webpack-padded.png │ ├── webpack.png │ ├── yarn-padded-90.png │ ├── yarn-padded.png │ └── yarn.png ├── jest │ └── setEnvVars.js ├── mocks │ └── fileMock.js └── scripts │ ├── .eslintrc │ ├── BabelRegister.js │ ├── CheckBuildsExist.js │ ├── CheckNativeDep.js │ ├── CheckNodeEnv.js │ ├── CheckPortInUse.js │ ├── CheckYarn.js │ ├── DeleteSourceMaps.js │ └── ElectronRebuild.js ├── package.json ├── resources ├── icon.icns ├── icon.ico ├── icon.png └── icons │ ├── icon_1024x1024.png │ ├── icon_128x128.png │ ├── icon_16x16.png │ ├── icon_24x24.png │ ├── icon_256x256.png │ ├── icon_32x32.png │ ├── icon_48x48.png │ ├── icon_512x512.png │ ├── icon_64x64.png │ └── icon_96x96.png ├── sonar-project.properties ├── test ├── .eslintrc.json ├── integration │ └── app │ │ └── server │ │ └── index.spec.ts └── ux │ └── Stepper.ux.ts ├── tsconfig.json ├── wiki └── Home.md └── yarn.lock /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: 4 | default: 5 | enabled: no 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # App packaged 34 | release 35 | app/main.prod.js 36 | app/main.prod.js.map 37 | app/renderer.prod.js 38 | app/renderer.prod.js.map 39 | app/style.css 40 | app/style.css.map 41 | dist 42 | dll 43 | main.js 44 | main.js.map 45 | 46 | .idea 47 | npm-debug.log.* 48 | .*.dockerfile 49 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # App packaged 34 | release 35 | app/*.main.prod.js 36 | app/main.prod.js 37 | app/main.prod.js.map 38 | app/renderer.prod.js 39 | app/renderer.prod.js.map 40 | app/style.css 41 | app/style.css.map 42 | dist 43 | dll 44 | main.js 45 | main.js.map 46 | 47 | .idea 48 | npm-debug.log.* 49 | __snapshots__ 50 | 51 | # Package.json 52 | package.json 53 | .travis.yml 54 | *.css.d.ts 55 | *.sass.d.ts 56 | *.scss.d.ts 57 | 58 | 59 | # Entire app/client directory 60 | app/client 61 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb/typescript', 3 | rules: { 4 | // A temporary hack related to IDE not resolving correct package.json 5 | 'import/no-extraneous-dependencies': 'off', 6 | }, 7 | parserOptions: { 8 | ecmaVersion: 2020, 9 | sourceType: 'module', 10 | project: './tsconfig.json', 11 | tsconfigRootDir: __dirname, 12 | createDefaultProgram: true, 13 | }, 14 | settings: { 15 | 'import/resolver': { 16 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 17 | node: {}, 18 | webpack: { 19 | config: require.resolve('./configs/webpack.config.eslint.js'), 20 | }, 21 | }, 22 | 'import/parsers': { 23 | '@typescript-eslint/parser': ['.ts', '.tsx'], 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.png binary 3 | *.jpg binary 4 | *.jpeg binary 5 | *.ico binary 6 | *.icns binary 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # TODO: edit funding when ready 2 | 3 | # These are supported funding model platforms 4 | 5 | # github: [deskreen, pavlobu] 6 | patreon: deskreen 7 | open_collective: deskreen 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: You're having technical issues. 🐞 And willing to share more details. If you don't know details, write here https://github.com/pavlobu/deskreen/discussions/68 4 | labels: 'bug' 5 | --- 6 | 7 | 8 | 9 | ## Prerequisites 10 | 11 | 12 | 13 | - [ ] Using yarn 14 | - [ ] Using an up-to-date [`master` branch](https://github.com/pavlobu/deskreen/tree/master) 15 | - [ ] Using latest version of devtools. [Check the docs for how to update](https://electron-react-boilerplate.js.org/docs/dev-tools/) 16 | - [ ] For issue in production release, add devtools output of `DEBUG_PROD=true yarn build && yarn start` 17 | 18 | ## Expected Behavior 19 | 20 | 21 | 22 | ## Current Behavior 23 | 24 | 25 | 26 | ## Steps to Reproduce 27 | 28 | 29 | 30 | 31 | 1. 32 | 33 | 2. 34 | 35 | 3. 36 | 37 | 4. 38 | 39 | ## Possible Solution (Not obligatory) 40 | 41 | 42 | 43 | ## Context 44 | 45 | 46 | 47 | 48 | 49 | ## Your Environment 50 | 51 | 52 | 53 | - Node version : 54 | - Deskreen version or branch : 55 | - Operating System and version : 56 | - Link to your project : 57 | 58 | 67 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You want something small added to Deskreen and with concrete code and solution. 🎉 If it is a big enhancement drop it here https://github.com/pavlobu/deskreen/discussions/50 4 | labels: 'enhancement' 5 | --- 6 | 7 | # Here you post only concrete examples of enhancements with code and solutions that you have. Other BIG enhancements and general ideas of features you would like to see in Deskreen, you post here: https://github.com/pavlobu/deskreen/discussions/50 8 | 9 | # Otherwise this issue will be closed. 10 | 11 | 20 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | requiredHeaders: 2 | - Prerequisites 3 | - Expected Behavior 4 | - Current Behavior 5 | - Possible Solution 6 | - Your Environment 7 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pr 8 | - discussion 9 | - e2e 10 | - enhancement 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | #name: codecov 2 | # 3 | #on: 4 | # push: 5 | # branches: 6 | # - master 7 | # pull_request: 8 | # branches: 9 | # - master 10 | # 11 | #jobs: 12 | # run: 13 | # runs-on: ubuntu-18.04 # IMPORTANT!!! this LINUX os should be the same as in build-and-test and release workflows! this is for making sure caches are used in most efficient way 14 | # 15 | # steps: 16 | # - name: Check out Git repository 17 | # uses: actions/checkout@v2.3.4 18 | # 19 | # - name: Setup Node.js environment 20 | # uses: actions/setup-node@v2.1.4 21 | # with: 22 | # node-version: '12' 23 | # 24 | # - name: Get yarn cache directory path 25 | # id: yarn-cache-dir-path 26 | # run: echo "::set-output name=dir::$(yarn cache dir)" 27 | # 28 | # - uses: actions/cache@v2.1.3 29 | # id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 30 | # with: 31 | # path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 32 | # key: ubuntu-18.04-yarn-${{ hashFiles('**/yarn.lock') }} 33 | # restore-keys: | 34 | # ubuntu-18.04-yarn- 35 | # 36 | # - name: yarn install in ./app/client 37 | # run: | 38 | # cd ./app/client 39 | # yarn install --frozen-lockfile 40 | # 41 | # - name: yarn install in ./ 42 | # run: yarn install --frozen-lockfile 43 | # 44 | # - name: yarn install in ./app 45 | # run: | 46 | # cd ./app 47 | # yarn install --frozen-lockfile 48 | # 49 | # - name: yarn build 50 | # env: 51 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | # run: yarn build 53 | # 54 | # - name: yarn test 55 | # run: yarn test 56 | # 57 | # - name: yarn coverage 58 | # run: yarn coverage 59 | # 60 | # - name: Upload app coverage to Codecov 61 | # uses: codecov/codecov-action@v1.0.12 62 | # with: 63 | # token: ${{ secrets.CODECOV_TOKEN }} 64 | # file: ./coverage/clover.xml 65 | # flags: unittests 66 | # name: codecov-umbrella 67 | # fail_ci_if_error: true 68 | # 69 | # - name: Upload app/client coverage to Codecov 70 | # uses: codecov/codecov-action@v1.0.12 71 | # with: 72 | # token: ${{ secrets.CODECOV_TOKEN }} 73 | # file: ./app/client/coverage/clover.xml 74 | # flags: unittests 75 | # name: codecov-umbrella 76 | # fail_ci_if_error: true 77 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-wiki.yml: -------------------------------------------------------------------------------- 1 | name: Publish wiki 2 | on: 3 | push: 4 | branches: [master] 5 | paths: 6 | - wiki/** 7 | - .github/workflows/publish-wiki.yml 8 | concurrency: 9 | group: publish-wiki 10 | cancel-in-progress: true 11 | permissions: 12 | contents: write 13 | jobs: 14 | publish-wiki: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: Andrew-Chen-Wang/github-wiki-action@v4 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | .scannerwork/ 3 | test-reports/ 4 | test-reporter.xml 5 | 6 | app/client/node_modules 7 | app/client/build 8 | 9 | # Logs 10 | logs 11 | *.log 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | .eslintcache 33 | 34 | # Dependency directory 35 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 36 | node_modules 37 | 38 | # OSX 39 | .DS_Store 40 | 41 | # App packaged 42 | release 43 | app/main.prod.js 44 | app/main.prod.js.map 45 | app/renderer.prod.js 46 | app/renderer.prod.js.map 47 | app/style.css 48 | app/style.css.map 49 | dist 50 | dll 51 | main.js 52 | main.js.map 53 | 54 | .idea 55 | npm-debug.log.* 56 | *.css.d.ts 57 | *.sass.d.ts 58 | *.scss.d.ts 59 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "EditorConfig.EditorConfig", 5 | "msjsdiag.debugger-for-chrome" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Deskreen: Main", 8 | "protocol": "inspector", 9 | "runtimeExecutable": "yarn", 10 | "runtimeArgs": ["start-main-debug"], 11 | "preLaunchTask": "Start Webpack Dev" 12 | }, 13 | { 14 | "name": "Deskreen: Renderer", 15 | "type": "chrome", 16 | "request": "attach", 17 | "port": 9223, 18 | "webRoot": "${workspaceFolder}", 19 | "timeout": 15000 20 | } 21 | ], 22 | "compounds": [ 23 | { 24 | "name": "Deskreen: All", 25 | "configurations": ["Deskreen: Main", "Deskreen: Renderer"] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".babelrc": "jsonc", 4 | ".eslintrc": "jsonc", 5 | ".prettierrc": "jsonc", 6 | ".stylelintrc": "json", 7 | ".dockerignore": "ignore", 8 | ".eslintignore": "ignore" 9 | }, 10 | 11 | "javascript.validate.enable": false, 12 | "javascript.format.enable": false, 13 | "typescript.format.enable": false, 14 | 15 | "search.exclude": { 16 | ".git": true, 17 | ".eslintcache": true, 18 | "app/dist": true, 19 | "app/main.prod.js": true, 20 | "app/main.prod.js.map": true, 21 | "bower_components": true, 22 | "dll": true, 23 | "release": true, 24 | "node_modules": true, 25 | "npm-debug.log.*": true, 26 | "test/**/__snapshots__": true, 27 | "yarn.lock": true, 28 | "*.{css,sass,scss}.d.ts": true 29 | }, 30 | "editor.codeActionsOnSave": { 31 | "source.fixAll.eslint": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "label": "Start Webpack Dev", 7 | "script": "start-renderer-dev", 8 | "options": { 9 | "cwd": "${workspaceFolder}" 10 | }, 11 | "isBackground": true, 12 | "problemMatcher": { 13 | "owner": "custom", 14 | "pattern": { 15 | "regexp": "____________" 16 | }, 17 | "background": { 18 | "activeOnStart": true, 19 | "beginsPattern": "Compiling\\.\\.\\.$", 20 | "endsPattern": "(Compiled successfully|Failed to compile)\\.$" 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /app/.testcafe-electron-rc: -------------------------------------------------------------------------------- 1 | { 2 | "mainWindowUrl": "./app.html", 3 | "appPath": "." 4 | } 5 | -------------------------------------------------------------------------------- /app/Routes.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint react/jsx-props-no-spreading: off */ 3 | import React from 'react'; 4 | import { Switch, Route } from 'react-router-dom'; 5 | import routes from './constants/routes.json'; 6 | import App from './containers/App'; 7 | import HomePage from './containers/HomePage'; 8 | 9 | export default function Routes() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/api/config.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | let host; 4 | let protocol; 5 | let port; 6 | 7 | if (!host && !protocol && !port) { 8 | host = '127.0.0.1'; 9 | protocol = 'http'; 10 | port = 3131; // TODO: read port from signaling server api 11 | } 12 | 13 | export default { 14 | host, 15 | port, 16 | protocol, 17 | }; 18 | -------------------------------------------------------------------------------- /app/api/urlGenerator.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | import config from './config'; 4 | 5 | export default (resourceName = '') => { 6 | const { port, protocol, host } = config; 7 | 8 | const resourcePath = resourceName; 9 | 10 | if (!host) { 11 | return `/localhost`; 12 | } 13 | 14 | return `${protocol}://${host}:${port}/${resourcePath}`; 15 | }; 16 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Deskreen 6 | 20 | 21 | 22 |
23 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavlobu/deskreen/4a87fc16d4c448cf9936a63fd84c1712bb5016ee/app/app.icns -------------------------------------------------------------------------------- /app/client/.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: 4 | default: 5 | enabled: no 6 | -------------------------------------------------------------------------------- /app/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /app/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deskreen-client-viewer", 3 | "version": "2.0.3", 4 | "private": true, 5 | "dependencies": { 6 | "@blueprintjs/core": "^3.35.0", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "@types/jest": "^24.0.0", 11 | "@types/node": "^12.0.0", 12 | "@types/react": "^16.9.0", 13 | "@types/react-dom": "^16.9.0", 14 | "@types/ua-parser-js": "^0.7.33", 15 | "i18next": "^19.8.4", 16 | "i18next-http-backend": "^1.0.21", 17 | "jest-sonar-reporter": "^2.0.0", 18 | "node-forge": "^0.9.1", 19 | "pixelmatch": "^5.2.1", 20 | "react": "^16.13.1", 21 | "react-app-polyfill": "^2.0.0", 22 | "react-dom": "^16.13.1", 23 | "react-flexbox-grid": "^2.1.2", 24 | "react-i18next": "^11.7.4", 25 | "react-player": "^2.6.2", 26 | "react-reveal": "^1.2.2", 27 | "react-scripts": "3.4.3", 28 | "react-spinners": "^0.9.0", 29 | "screenfull": "^5.0.2", 30 | "shortid": "^2.2.15", 31 | "socket.io-client": "^2.3.0", 32 | "typescript": "~3.7.2", 33 | "ua-parser-js": "^0.7.22" 34 | }, 35 | "scripts": { 36 | "start": "react-scripts start", 37 | "build": "react-scripts build", 38 | "test": "react-scripts test", 39 | "eject": "react-scripts eject", 40 | "test:nowatch": "react-scripts test --watchAll=false", 41 | "coverage": "yarn test -- --coverage --watchAll=false" 42 | }, 43 | "eslintConfig": { 44 | "extends": "react-app" 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | }, 58 | "devDependencies": { 59 | "@types/node-forge": "^0.9.5", 60 | "@types/pixelmatch": "^5.2.2", 61 | "@types/resemblejs": "^1.3.29", 62 | "@types/simple-peer": "^9.6.0", 63 | "@types/socket.io-client": "^1.4.33" 64 | }, 65 | "jest": { 66 | "collectCoverageFrom": [ 67 | "src/**/*.{js,jsx,ts,tsx}", 68 | "!/node_modules/", 69 | "!/src/serviceWorker.ts" 70 | ], 71 | "coverageThreshold": { 72 | "global": { 73 | "branches": 0, 74 | "functions": 0, 75 | "lines": 0, 76 | "statements": 0 77 | } 78 | }, 79 | "coverageReporters": [ 80 | "json", 81 | "lcov", 82 | "text", 83 | "clover" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavlobu/deskreen/4a87fc16d4c448cf9936a63fd84c1712bb5016ee/app/client/public/favicon.ico -------------------------------------------------------------------------------- /app/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Deskreen Viewer 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/client/public/locales/zh_CN/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Waiting for user to click ALLOW button on screen sharing device...": "正在等待用户单击屏幕共享设备上的允许按钮...", 3 | "Waiting for user to select source to share from screen sharing device...": "正在等待用户从屏幕共享设备选择要共享的源...", 4 | "My Device Info": "我的设备信息", 5 | "Device Type": "设备类型", 6 | "Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen is running": "您的设备 IP 应该与运行 Deskreen 的计算机上出现的警报弹出窗口中的 '设备 IP' 相匹配。", 7 | "Device IP": "设备 IP", 8 | "Device Browser": "设备浏览器", 9 | "Device OS": "设备操作系统", 10 | "These details should match with the ones that you see in alert popup on computer screen, where Deskreen is running": "这些详细信息应与您在屏幕共享设备上的警报弹出窗口中看到的信息相匹配。", 11 | "Deskreen Screen Viewer": "Deskreen 屏幕查看器", 12 | "Connected!": "已连接!", 13 | "Error occurred": "出现错误", 14 | "Deskreen Error Dialog": "Deskreen 错误对话框", 15 | "Something went wrong": "出问题了", 16 | "You may close this browser window then try to connect again": "您可以关闭此浏览器窗口,然后尝试重新连接", 17 | "An unknown error occurred": "出现未知错误", 18 | "You were not allowed to connect": "您不能连接", 19 | "You were disconnected": "您将断开连接", 20 | "WebRTC error occurred": "出现 WebRTC 错误", 21 | "If you like Deskreen consider contributing financially Deskreen is open-source Your donations keep us motivated to make Deskreen even better": "如果你喜欢 Deskreen,可以考虑出钱。Deskreen 是开源的。您的捐赠使我们有动力让 Deskreen 变得更好。", 22 | "Donate": "捐赠", 23 | "Video stream is paused": "视频流暂停", 24 | "Video stream is playing": "正在播放视频流", 25 | "Pause": "暂停", 26 | "Play": "播放", 27 | "Video Settings": "视频设置", 28 | "Flip": "翻转", 29 | "Video quality has been changed to": "视频质量已更改为", 30 | "Click to Open Video Settings": "单击以打开视频设置", 31 | "Click to Enter Full Screen Mode": "单击以进入全屏模式", 32 | "Default video player has been turned OFF": "默认视频播放器已关闭", 33 | "Default video player has been turned ON": "默认视频播放器已开启", 34 | "ON": "开启", 35 | "OFF": "关闭", 36 | "Default Video Player": "默认视频播放器", 37 | "Click to visit our website": "点击访问我们的网站", 38 | "Video is flipped horizontally": "视频水平翻转", 39 | "TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "以下翻译尚未添加到 UI,但欢迎您的翻译!功能将很快添加,因此需要您的翻译", 40 | "Click to see connection info": "单击以查看连接信息", 41 | "Pair ID": "配对 ID", 42 | "Unpair": "取消配对", 43 | "Session ID": "会话 ID", 44 | "Click to boost video stream if it is lagging": "如果视频流滞后,请单击以提高视频流" 45 | } 46 | -------------------------------------------------------------------------------- /app/client/public/locales/zh_TW/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Waiting for user to click ALLOW button on screen sharing device...": "正在等待使用者單擊螢幕共享裝置上的允許按鈕...", 3 | "Waiting for user to select source to share from screen sharing device...": "正在等待使用者從螢幕共享裝置選擇要共享的源...", 4 | "My Device Info": "我的裝置資訊", 5 | "Device Type": "裝置型別", 6 | "Your Device IP should match with Device IP in alert popup appeared on your computer, where Deskreen is running": "您的裝置 IP 應該與執行 Deskreen 的計算機上出現的警報彈出視窗中的 '裝置 IP' 相匹配。", 7 | "Device IP": "裝置 IP", 8 | "Device Browser": "裝置瀏覽器", 9 | "Device OS": "裝置作業系統", 10 | "These details should match with the ones that you see in alert popup on computer screen, where Deskreen is running": "這些詳細資訊應與您在螢幕共享裝置上的警報彈出視窗中看到的資訊相匹配。", 11 | "Deskreen Screen Viewer": "Deskreen 螢幕檢視器", 12 | "Connected!": "已連線!", 13 | "Error occurred": "出現錯誤", 14 | "Deskreen Error Dialog": "Deskreen 錯誤對話方塊", 15 | "Something went wrong": "出問題了", 16 | "You may close this browser window then try to connect again": "您可以關閉此瀏覽器視窗,然後嘗試重新連線", 17 | "An unknown error occurred": "出現未知錯誤", 18 | "You were not allowed to connect": "您不能連線", 19 | "You were disconnected": "您將斷開連線", 20 | "WebRTC error occurred": "出現 WebRTC 錯誤", 21 | "If you like Deskreen consider contributing financially Deskreen is open-source Your donations keep us motivated to make Deskreen even better": "如果你喜歡 Deskreen,可以考慮出錢。Deskreen 是開源的。您的捐贈使我們有動力讓 Deskreen 變得更好。", 22 | "Donate": "捐贈", 23 | "Video stream is paused": "影片流暫停", 24 | "Video stream is playing": "正在播放影片流", 25 | "Pause": "暫停", 26 | "Play": "播放", 27 | "Video Settings": "影片設定", 28 | "Flip": "翻轉", 29 | "Video quality has been changed to": "影片質量已更改為", 30 | "Click to Open Video Settings": "單擊以開啟影片設定", 31 | "Click to Enter Full Screen Mode": "單擊以進入全屏模式", 32 | "Default video player has been turned OFF": "預設影片播放器已關閉", 33 | "Default video player has been turned ON": "預設影片播放器已開啟", 34 | "ON": "開啟", 35 | "OFF": "關閉", 36 | "Default Video Player": "預設影片播放器", 37 | "Click to visit our website": "點選訪問我們的網站", 38 | "Video is flipped horizontally": "影片水平翻轉", 39 | "TRANSLATIONS BELOW ARE NOT ADDED TO UI YET, BUT YOUR TRANSLATIONS ARE WELCOME! THE FEATURES WILL BE ADDED SOON SO YOUR TRANSLATIONS ARE NEEDED": "以下翻譯尚未新增到 UI,但歡迎您的翻譯!功能將很快新增,因此需要您的翻譯", 40 | "Click to see connection info": "單擊以檢視連線資訊", 41 | "Pair ID": "配對 ID", 42 | "Unpair": "取消配對", 43 | "Session ID": "會話 ID", 44 | "Click to boost video stream if it is lagging": "如果影片流滯後,請單擊以提高影片流" 45 | } 46 | -------------------------------------------------------------------------------- /app/client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavlobu/deskreen/4a87fc16d4c448cf9936a63fd84c1712bb5016ee/app/client/public/logo192.png -------------------------------------------------------------------------------- /app/client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavlobu/deskreen/4a87fc16d4c448cf9936a63fd84c1712bb5016ee/app/client/public/logo512.png -------------------------------------------------------------------------------- /app/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Deskreen", 3 | "name": "Deskreen Makes Any Device a Second Screen For Your Computer", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /app/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/client/sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=deskreen-viewer 2 | sonar.typescript.lcov.reportPaths=coverage/lcov.info 3 | sonar.sources=src 4 | sonar.cpd.exclusions=src/config/*,src/**/__mocks__/*,src/**/mocks/*,src/**/*.spec.ts,src/**/*.spec.tsx,src/**/*.test.ts,src/**/*.test.tsx,src/serviceWorker.ts,src/index.tsx 5 | sonar.coverage.exclusions=src/config/*,src/**/__mocks__/*,src/**/mocks/*,src/**/*.spec.ts,src/**/*.spec.tsx,src/**/*.test.ts,src/**/*.test.tsx,src/serviceWorker.ts,src/index.tsx 6 | sonar.host.url=http://localhost:9000 7 | sonar.login=e3b5f73b8778290f7074c40a4159c32b7f15a8e6 8 | sonar.exclusions=src/serviceWorker.ts,node_modules/** 9 | -------------------------------------------------------------------------------- /app/client/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | // const { getByText } = render(); 7 | // const linkElement = getByText(/learn react/i); 8 | // expect(linkElement).toBeInTheDocument(); 9 | expect(true).toBe(true); 10 | }); 11 | -------------------------------------------------------------------------------- /app/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MainView from './containers/MainView'; 3 | 4 | function App() { 5 | return ( 6 | 7 | ); 8 | } 9 | 10 | export default App; 11 | -------------------------------------------------------------------------------- /app/client/src/api/config.ts: -------------------------------------------------------------------------------- 1 | let host; 2 | let protocol; 3 | let port; 4 | 5 | if (!host && !protocol && !port) { 6 | host = window.location.host.split(':')[0]; 7 | protocol = 'http'; 8 | port = 3131; 9 | } 10 | 11 | export default { 12 | host, 13 | port, 14 | protocol, 15 | }; 16 | -------------------------------------------------------------------------------- /app/client/src/api/generator.spec.ts: -------------------------------------------------------------------------------- 1 | import generator from './generator'; 2 | import { TEST_PORT, TEST_HOST, TEST_PROTOCOL } from './mocks/generatorTestVariables'; 3 | 4 | // how to use local variables in jest mock to get rid of hoisting mocks to top most code block: 5 | //stackoverflow.com/questions/44649699/service-mocked-with-jest-causes-the-module-factory-of-jest-mock-is-not-allowe 6 | jest.mock('./config', () => { 7 | const generatorTestVariables = require('./mocks/generatorTestVariables'); 8 | 9 | return { 10 | host: generatorTestVariables.TEST_HOST, 11 | protocol: generatorTestVariables.TEST_PROTOCOL, 12 | port: generatorTestVariables.TEST_PORT, 13 | }; 14 | }); 15 | 16 | 17 | describe('generator.ts', () => { 18 | afterEach(() => { 19 | jest.clearAllMocks(); 20 | jest.restoreAllMocks(); 21 | }); 22 | 23 | describe('when generator() is called properly', () => { 24 | it('should produce correct string', () => { 25 | const roomID = '333'; 26 | 27 | const result = generator(roomID); 28 | 29 | expect(result).toMatch( 30 | `${TEST_PROTOCOL}://${TEST_HOST}:${TEST_PORT}/${roomID}` 31 | ); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /app/client/src/api/generator.ts: -------------------------------------------------------------------------------- 1 | import config from './config'; 2 | 3 | export default (resourceName = '') => { 4 | const { port, protocol, host } = config; 5 | 6 | const resourcePath = resourceName; 7 | 8 | if (!host) { 9 | return `/localhost`; 10 | } 11 | 12 | return `${protocol}://${host}:${port}/${resourcePath}`; 13 | }; 14 | -------------------------------------------------------------------------------- /app/client/src/api/mocks/generatorTestVariables.ts: -------------------------------------------------------------------------------- 1 | export const TEST_PORT = '3232'; 2 | export const TEST_HOST = '123.123.123.123'; 3 | export const TEST_PROTOCOL = 'http'; 4 | -------------------------------------------------------------------------------- /app/client/src/components/ConnectingIndicator/ConnectingIndicatorIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from '@blueprintjs/core'; 3 | import { Col, Row } from 'react-flexbox-grid'; 4 | 5 | interface ConnectingIndicatorIconProps { 6 | connectionIconType: "feed" | "feed-subscribed"; 7 | } 8 | 9 | function ConnectingIndicatorIcon(props: ConnectingIndicatorIconProps) { 10 | const { connectionIconType } = props; 11 | 12 | return ( 13 | 21 | 22 | 30 | 31 | 32 | ); 33 | } 34 | 35 | export default ConnectingIndicatorIcon; 36 | -------------------------------------------------------------------------------- /app/client/src/components/ConnectingIndicator/LoadingSharingIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Icon } from '@blueprintjs/core'; 3 | import { Col, Row } from 'react-flexbox-grid'; 4 | import PropagateLoader from 'react-spinners/PropagateLoader'; 5 | import { AppContext } from '../../providers/AppContextProvider'; 6 | 7 | const Fade = require('react-reveal/Fade'); 8 | 9 | interface SelectSharingIconProps { 10 | loadingSharingIconType: LoadingSharingIconType; 11 | isShownLoadingSharingIcon: boolean; 12 | } 13 | 14 | function LoadingSharingIcon(props: SelectSharingIconProps) { 15 | const { isDarkTheme } = useContext(AppContext); 16 | 17 | const { 18 | loadingSharingIconType: selectingSharingIconType, 19 | isShownLoadingSharingIcon: isShownSelectingSharingIcon, 20 | } = props; 21 | 22 | return ( 23 | 33 | 34 | 41 | 42 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | export default LoadingSharingIcon; 66 | -------------------------------------------------------------------------------- /app/client/src/components/ErrorDialog/ErrorMessageEnum.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorMessage { 2 | UNKNOWN_ERROR = 'An unknown error occurred', 3 | DENY_TO_CONNECT = 'You were not allowed to connect', 4 | DISCONNECTED = 'You were disconnected', 5 | NOT_ALLOWED = 'You were not allowed to connect', 6 | WEBRTC_ERROR = 'WebRTC error occurred' 7 | } 8 | -------------------------------------------------------------------------------- /app/client/src/components/ErrorDialog/index.css: -------------------------------------------------------------------------------- 1 | .error-dialog-backdrop { 2 | backdrop-filter: blur(2px); 3 | } 4 | -------------------------------------------------------------------------------- /app/client/src/components/ErrorDialog/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Classes, 4 | Dialog, 5 | Divider, 6 | H1, 7 | H2, 8 | H3, 9 | Icon, 10 | } from '@blueprintjs/core'; 11 | import { Col, Row } from 'react-flexbox-grid'; 12 | import { useTranslation } from 'react-i18next'; 13 | import './index.css'; 14 | import { ErrorMessage } from './ErrorMessageEnum'; 15 | 16 | interface ErrorDialogProps { 17 | isOpen: boolean; 18 | errorMessage: ErrorMessage; 19 | } 20 | 21 | function ErrorDialog(props: ErrorDialogProps) { 22 | const { t } = useTranslation(); 23 | const { errorMessage, isOpen } = props; 24 | 25 | return ( 26 | 40 | 41 | 42 |

{t('Deskreen Error Dialog')}

43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |

56 | {`${t('Something went wrong')} :(`} 57 |

58 | 59 |
60 | 61 |
62 | 63 |
64 |

{t(`${errorMessage}`)}

65 | 66 |

{`${t('You may close this browser window then try to connect again')}.`}

67 |
68 |
69 | ); 70 | } 71 | 72 | export default ErrorDialog; 73 | -------------------------------------------------------------------------------- /app/client/src/components/MyDeviceInfoCard/DeviceDetails.d.ts: -------------------------------------------------------------------------------- 1 | interface DeviceDetails { 2 | myIP: string, 3 | myOS: string, 4 | myDeviceType: string, 5 | myBrowser: string, 6 | } 7 | -------------------------------------------------------------------------------- /app/client/src/components/MyDeviceInfoCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Callout, Card, H3, Text, Tooltip, Position } from '@blueprintjs/core'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { AppContext } from '../../providers/AppContextProvider'; 5 | import { 6 | DARK_UI_BACKGROUND, 7 | LIGHT_UI_BACKGROUND, 8 | } from '../../constants/styleConstants'; 9 | 10 | interface MyDeviceDetailsCardProps { 11 | deviceDetails: DeviceDetails; 12 | } 13 | 14 | function MyDeviceInfoCard(props: MyDeviceDetailsCardProps) { 15 | const { t } = useTranslation(); 16 | const { isDarkTheme } = useContext(AppContext); 17 | 18 | const { deviceDetails } = props; 19 | const { myIP, myOS, myDeviceType, myBrowser } = deviceDetails; 20 | 21 | return ( 22 | 29 |

{`${t('My Device Info')}:`}

30 | 31 | {`${t('Device Type')}: ${myDeviceType}`} 32 | 36 |
45 | {`${t('Device IP')}: ${myIP}`} 46 |
47 |
48 | {`${t('Device Browser')}: ${myBrowser}`} 49 | {`${t('Device OS')}: ${myOS}`} 50 |
51 | 52 | {t('These details should match with the ones that you see in alert popup on computer screen, where Deskreen is running')} 53 | 54 |
55 | ); 56 | } 57 | 58 | export default MyDeviceInfoCard; 59 | -------------------------------------------------------------------------------- /app/client/src/components/PlayerControlPanel/handlePlayerToggleFullscreen.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | // IMPORTANT! leave upper blank line so this file is ignored for coverage!!! More on this issue here 4 | // https://github.com/facebook/create-react-app/issues/6106#issuecomment-550076629 5 | import { REACT_PLAYER_WRAPPER_ID } from "../../constants/appConstants"; 6 | 7 | export default () => { 8 | const player = document.querySelector( 9 | `#${REACT_PLAYER_WRAPPER_ID} > video` 10 | ); 11 | if (!player) return; 12 | // @ts-ignore 13 | if (player.requestFullScreen) { 14 | // @ts-ignore 15 | player.requestFullScreen(); 16 | // @ts-ignore 17 | } else if (player.webkitRequestFullScreen) { 18 | // @ts-ignore 19 | player.webkitRequestFullScreen(); 20 | // @ts-ignore 21 | } else if (player.mozRequestFullScreen) { 22 | // @ts-ignore 23 | player.mozRequestFullScreen(); 24 | // @ts-ignore 25 | } else if (player.msRequestFullscreen) { 26 | // @ts-ignore 27 | player.msRequestFullscreen(); 28 | // @ts-ignore 29 | } else if (player.webkitEnterFullscreen) { 30 | // @ts-ignore 31 | player.webkitEnterFullscreen(); //for iphone this code worked 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /app/client/src/components/PlayerControlPanel/index.css: -------------------------------------------------------------------------------- 1 | .play-pause-text { 2 | width: max-content; 3 | } 4 | -------------------------------------------------------------------------------- /app/client/src/components/PlayerControlPanel/initScreenfullOnChange.ts: -------------------------------------------------------------------------------- 1 | import screenfull from "screenfull"; 2 | 3 | export default (setIsFullScreenOn: (_: boolean) => void) => { 4 | if (!screenfull.isEnabled) return; 5 | // @ts-ignore 6 | screenfull.on('change', () => { 7 | // @ts-ignore 8 | setIsFullScreenOn(screenfull.isFullscreen); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /app/client/src/components/ToggleDarkModeSwitch/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Switch, Classes, Alignment } from '@blueprintjs/core'; 3 | import { AppContext } from '../../providers/AppContextProvider'; 4 | 5 | function ToggleDarkModeSwitch() { 6 | const { isDarkTheme, setIsDarkThemeHook } = useContext(AppContext) 7 | 8 | return ( 9 | { 11 | document.body.classList.toggle(Classes.DARK); 12 | setIsDarkThemeHook(!isDarkTheme) 13 | }} 14 | innerLabel={isDarkTheme ? 'ON' : 'OFF'} 15 | inline 16 | large 17 | label={`Dark Theme is`} 18 | alignIndicator={Alignment.RIGHT} 19 | checked={isDarkTheme} 20 | style={{ marginTop: '25px', marginLeft: '15px' }} 21 | /> 22 | ); 23 | } 24 | 25 | export default ToggleDarkModeSwitch; 26 | -------------------------------------------------------------------------------- /app/client/src/config/i18n.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | import i18n from 'i18next'; 4 | import { initReactI18next } from 'react-i18next'; 5 | import Backend from 'i18next-http-backend'; 6 | 7 | // don't want to use this? 8 | // have a look at the Quick start guide 9 | // for passing in lng and translations on init 10 | 11 | i18n 12 | // load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) 13 | // learn more: https://github.com/i18next/i18next-http-backend 14 | .use(Backend) 15 | // pass the i18n instance to react-i18next. 16 | .use(initReactI18next) 17 | // init i18next 18 | // for all options read: https://www.i18next.com/overview/configuration-options 19 | .init({ 20 | lng: 'en', 21 | saveMissing: true, 22 | saveMissingTo: 'all', 23 | fallbackLng: 'en', // TODO: to generate missing keys use false as value here, will be useful when custom nodejs server is created to store missing values 24 | debug: false, // change to true to see debug message logs in browser console 25 | whitelist: ['en', 'es', 'ru', 'ua', 'zh_CN', 'zh_TW', 'da', 'de', 'fi', 'ko', 'it', 'ja', 'nl', 'fr', 'sv'], 26 | backend: { 27 | // path where resources get loaded from 28 | loadPath: '/locales/{{lng}}/{{ns}}.json', 29 | // TODO: in future implement custom nodejs server that accepts missing translations POST requests and updates .missing.json files accordingly. Here is how to do so: https://www.robinwieruch.de/react-internationalization . it can be simple nodejs server that can be started when 'yarn dev' is running, need to ckagne package.json file then 30 | // path to post missing resources 31 | addPath: '/locales/{{lng}}/{{ns}}.json', 32 | // jsonIndent to use when storing json files 33 | jsonIndent: 2, 34 | }, 35 | 36 | keySeparator: false, // we do not use keys in form messages.welcome 37 | 38 | interpolation: { 39 | escapeValue: false, // react already safes from xss 40 | }, 41 | }); 42 | 43 | export default i18n; 44 | -------------------------------------------------------------------------------- /app/client/src/constants/appConstants.ts: -------------------------------------------------------------------------------- 1 | export const REACT_PLAYER_WRAPPER_ID = 'react-player-wrapper-id'; 2 | export const VIDEO_QUALITY_TO_DECIMAL = { 3 | '25%': 0.25, // Q_25_PERCENT 4 | '40%': 0.40, // Q_40_PERCENT 5 | '60%': 0.60, // Q_60_PERCENT 6 | '80%': 0.80, // Q_80_PERCENT 7 | '100%': 1, // Q_100_PERCENT 8 | } 9 | export const COMPARISON_CANVAS_ID = 'comparison-canvas'; 10 | export const DUMMY_MY_DEVICE_DETAILS = { 11 | myIP: '', 12 | myOS: '', 13 | myDeviceType: '', 14 | myBrowser: '', 15 | }; 16 | -------------------------------------------------------------------------------- /app/client/src/constants/styleConstants.ts: -------------------------------------------------------------------------------- 1 | export const LIGHT_UI_BACKGROUND = 'rgba(240, 248, 250, 1)'; 2 | export const DARK_UI_BACKGROUND = '#293742'; 3 | -------------------------------------------------------------------------------- /app/client/src/containers/MainView/ConnectionIconEnum.ts: -------------------------------------------------------------------------------- 1 | enum ConnectionIcon { 2 | FEED = 'feed', 3 | FEED_SUBSCRIBED = 'feed-subscribed' 4 | } 5 | 6 | export default ConnectionIcon; 7 | -------------------------------------------------------------------------------- /app/client/src/containers/MainView/LoadingSharingIconEnum.ts: -------------------------------------------------------------------------------- 1 | enum LoadingSharingIconEnum { 2 | DESKTOP = 'desktop', 3 | APPLICATION = 'application' 4 | } 5 | 6 | export default LoadingSharingIconEnum; 7 | -------------------------------------------------------------------------------- /app/client/src/containers/MainView/changeLanguage.ts: -------------------------------------------------------------------------------- 1 | import i18n from "../../config/i18n"; 2 | 3 | export default (lng: string) => { 4 | i18n.changeLanguage(lng); 5 | }; 6 | -------------------------------------------------------------------------------- /app/client/src/containers/MainView/handleCreatePeerConnection.ts: -------------------------------------------------------------------------------- 1 | import PeerConnection from '../../features/PeerConnection'; 2 | import PeerConnectionUIHandler from '../../features/PeerConnection/PeerConnectionUIHandler'; 3 | import getRoomIDOfCurrentBrowserWindow from '../../utils/getRoomIDOfCurrentBrowserWindow'; 4 | import Crypto from '../../utils/crypto'; 5 | import VideoAutoQualityOptimizer from '../../features/VideoAutoQualityOptimizer'; 6 | import changeLanguage from './changeLanguage'; 7 | import ConnectionIcon from './ConnectionIconEnum'; 8 | 9 | export default (params: CreatePeerConnectionUseEffectParams) => { 10 | const { 11 | isDarkTheme, 12 | peer, 13 | setIsDarkThemeHook, 14 | setMyDeviceDetails, 15 | setConnectionIconType, 16 | setIsShownTextPrompt, 17 | setPromptStep, 18 | setScreenSharingSourceType, 19 | setDialogErrorMessage, 20 | setIsErrorDialogOpen, 21 | setUrl, 22 | setPeer, 23 | } = params; 24 | 25 | return () => { 26 | if (!peer) { 27 | const UIHandler = new PeerConnectionUIHandler( 28 | isDarkTheme, 29 | setMyDeviceDetails, 30 | () => { 31 | setConnectionIconType(ConnectionIcon.FEED_SUBSCRIBED); 32 | 33 | setIsShownTextPrompt(false); 34 | setIsShownTextPrompt(true); 35 | setPromptStep(2); 36 | 37 | setTimeout(() => { 38 | setIsShownTextPrompt(false); 39 | setIsShownTextPrompt(true); 40 | setPromptStep(3); 41 | }, 2000); 42 | }, 43 | setScreenSharingSourceType, 44 | setIsDarkThemeHook, 45 | changeLanguage, 46 | setDialogErrorMessage, 47 | setIsErrorDialogOpen 48 | ); 49 | 50 | const _peer = new PeerConnection( 51 | getRoomIDOfCurrentBrowserWindow(), 52 | setUrl, 53 | new Crypto(), 54 | new VideoAutoQualityOptimizer(), 55 | UIHandler 56 | ); 57 | 58 | setPeer(_peer); 59 | 60 | setTimeout(() => { 61 | setIsShownTextPrompt(true); 62 | }, 100); 63 | } 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /app/client/src/containers/MainView/handleDisplayingLoadingSharingIconLoop.spec.ts: -------------------------------------------------------------------------------- 1 | import handleDisplayingLoadingSharingIconLoop from './handleDisplayingLoadingSharingIconLoop'; 2 | import LoadingSharingIconEnum from './LoadingSharingIconEnum'; 3 | 4 | jest.useFakeTimers(); 5 | 6 | describe('handleDisplayingLoadingSharingIconLoop callback', () => { 7 | let testParams: handleDisplayingLoadingSharingIconLoopParams; 8 | 9 | beforeEach(() => { 10 | testParams = { 11 | promptStep: 3, 12 | url: undefined, 13 | setIsShownLoadingSharingIcon: jest.fn(), 14 | setLoadingSharingIconType: jest.fn(), 15 | loadingSharingIconType: LoadingSharingIconEnum.DESKTOP, 16 | isShownLoadingSharingIcon: false, 17 | } as handleDisplayingLoadingSharingIconLoopParams 18 | }); 19 | 20 | afterEach(() => { 21 | jest.clearAllMocks(); 22 | jest.restoreAllMocks(); 23 | }); 24 | 25 | describe('when it was called with promptStep 3 and url undefined', () => { 26 | it('it should start loop, the ui should change with interval, showing and hiding icons', () => { 27 | const callback = handleDisplayingLoadingSharingIconLoop(testParams); 28 | callback(); 29 | 30 | // imitation of infinite loop 31 | jest.advanceTimersByTime(2000); 32 | 33 | expect(testParams.setIsShownLoadingSharingIcon).toBeCalledWith(true); 34 | jest.clearAllMocks(); 35 | 36 | jest.advanceTimersByTime(2000); 37 | 38 | expect(testParams.setIsShownLoadingSharingIcon).toBeCalledWith(false); 39 | expect(testParams.setLoadingSharingIconType).toBeCalledWith(LoadingSharingIconEnum.APPLICATION); 40 | jest.clearAllMocks(); 41 | 42 | jest.advanceTimersByTime(2000); 43 | 44 | expect(testParams.setIsShownLoadingSharingIcon).toBeCalledWith(true); 45 | expect(testParams.setLoadingSharingIconType).toBeCalledWith(LoadingSharingIconEnum.DESKTOP); 46 | jest.clearAllMocks(); 47 | 48 | // ... by this time we are sure, this function works! 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /app/client/src/containers/MainView/handleDisplayingLoadingSharingIconLoop.ts: -------------------------------------------------------------------------------- 1 | import LoadingSharingIconEnum from './LoadingSharingIconEnum'; 2 | 3 | export default (params: handleDisplayingLoadingSharingIconLoopParams) => { 4 | const { 5 | promptStep, 6 | url, 7 | setIsShownLoadingSharingIcon, 8 | loadingSharingIconType, 9 | isShownLoadingSharingIcon, 10 | setLoadingSharingIconType, 11 | } = params; 12 | return () => { 13 | let interval: NodeJS.Timeout; 14 | if (promptStep === 3 && url === undefined) { 15 | setIsShownLoadingSharingIcon(true); 16 | 17 | let currentIcon = loadingSharingIconType; 18 | let isShownIcon = isShownLoadingSharingIcon; 19 | let isShownWithFadingUIEffect = false; 20 | interval = setInterval(() => { 21 | isShownIcon = !isShownIcon; 22 | setIsShownLoadingSharingIcon(isShownIcon); 23 | if (isShownWithFadingUIEffect) { 24 | currentIcon = 25 | currentIcon === LoadingSharingIconEnum.DESKTOP 26 | ? LoadingSharingIconEnum.APPLICATION 27 | : LoadingSharingIconEnum.DESKTOP; 28 | setLoadingSharingIconType(currentIcon); 29 | isShownWithFadingUIEffect = false; 30 | } else { 31 | isShownWithFadingUIEffect = true; 32 | } 33 | }, 1500); 34 | } 35 | 36 | return () => { 37 | clearInterval(interval); 38 | }; 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /app/client/src/containers/MainView/handleNoConnectionTimeout.ts: -------------------------------------------------------------------------------- 1 | import { DUMMY_MY_DEVICE_DETAILS } from "../../constants/appConstants"; 2 | 3 | export default ( 4 | myDeviceDetails: DeviceDetails, 5 | setIsErrorDialogOpen: (_: boolean) => void 6 | ) => { 7 | return () => { 8 | const timeout = setTimeout(() => { 9 | if (myDeviceDetails === DUMMY_MY_DEVICE_DETAILS) { 10 | setIsErrorDialogOpen(true); 11 | } 12 | }, 10000); 13 | return () => { 14 | clearTimeout(timeout); 15 | }; 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /app/client/src/containers/MainView/handleRemoveDanglingReactRevealContainer.ts: -------------------------------------------------------------------------------- 1 | export default ( 2 | url: undefined | MediaStream 3 | ) => { 4 | return () => { 5 | if (url !== undefined) { 6 | setTimeout(() => { 7 | // @ts-ignore 8 | document.querySelector('.container > .react-reveal').style.display = 9 | 'none'; 10 | }, 1000); 11 | } 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /app/client/src/containers/MainView/handleSetVideoQuality.ts: -------------------------------------------------------------------------------- 1 | import PeerConnection from "../../features/PeerConnection"; 2 | import { VideoQuality } from "../../features/VideoAutoQualityOptimizer/VideoQualityEnum"; 3 | 4 | export default ( 5 | videoQuality: VideoQuality, 6 | peer: PeerConnection | undefined 7 | ) => { 8 | return () => { 9 | if (!peer) return; 10 | if (!peer.isStreamStarted) return; 11 | peer.setVideoQuality(videoQuality); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /app/client/src/containers/MainView/index.css: -------------------------------------------------------------------------------- 1 | @media (prefers-reduced-motion: no-preference) { 2 | .App-logo { 3 | animation: App-logo-spin infinite 20s linear; 4 | } 5 | 6 | #pulsing-circle-1 { 7 | animation: pulse1 infinite 6s linear; 8 | } 9 | 10 | #pulsing-circle-2 { 11 | animation: pulse2 infinite 6s linear; 12 | } 13 | 14 | .pulse-3-once { 15 | animation: pulse3twice 2 750ms linear; 16 | } 17 | } 18 | 19 | @keyframes pulse1 { 20 | 0% { 21 | transform: scale(0.95); 22 | box-shadow: 0 0 0 0 rgba(72, 175, 240, 0.7); 23 | } 24 | 25 | 80% { 26 | transform: scale(1); 27 | box-shadow: 0 0 0 15px rgba(72, 175, 240, 0.4); 28 | } 29 | 30 | 100% { 31 | transform: scale(0.95); 32 | box-shadow: 0 0 0 0 rgba(72, 175, 240, 0); 33 | } 34 | } 35 | 36 | @keyframes pulse2 { 37 | 100% { 38 | transform: scale(0.95); 39 | box-shadow: 0 0 0 0 rgba(72, 175, 240, 0); 40 | } 41 | 42 | 80% { 43 | transform: scale(1); 44 | box-shadow: 0 0 0 33px rgba(72, 175, 240, 0.3); 45 | } 46 | 47 | 0% { 48 | transform: scale(0.95); 49 | box-shadow: 0 0 0 0 rgba(72, 175, 240, 1); 50 | } 51 | } 52 | 53 | @keyframes pulse3twice { 54 | 0% { 55 | box-shadow: 0 0 0 0 rgba(21, 179, 113, 0.7); 56 | } 57 | 58 | 50% { 59 | box-shadow: 0 0 0 30px rgba(21, 179, 113, 0.3); 60 | } 61 | 62 | 100% { 63 | box-shadow: 0 0 0 0 rgba(21, 179, 113, 0); 64 | } 65 | } 66 | 67 | 68 | .container > .react-reveal { 69 | overflow: hidden; 70 | } 71 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/NullUser.ts: -------------------------------------------------------------------------------- 1 | export default { username: '', publicKey: '', privateKey: '' }; 2 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/PartnerPeerUser.d.ts: -------------------------------------------------------------------------------- 1 | interface PartnerPeerUser { 2 | username: string; 3 | publicKey: string; 4 | } 5 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/PeerConnection.d.ts: -------------------------------------------------------------------------------- 1 | type PeerConnection = import('.').default; 2 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/PeerConnectionUIHandler.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum'; 2 | 3 | export default class PeerConnectionUIHandler { 4 | isDarkTheme: boolean; 5 | 6 | setMyDeviceDetails: (details: DeviceDetails) => void; 7 | 8 | hostAllowedToConnectCallback: () => void; 9 | 10 | setScreenSharingSourceTypeCallback: (s: ScreenSharingSourceType) => void; 11 | 12 | setIsDarkThemeCallback: (val: boolean) => void; 13 | 14 | setAppLanguageCallback: (newLang: string) => void; 15 | 16 | setDialogErrorMessageCallback: (message: ErrorMessage) => void; 17 | 18 | setIsErrorDialogOpen: (val: boolean) => void; 19 | 20 | errorDialogMessage = ErrorMessage.UNKNOWN_ERROR; 21 | 22 | constructor( 23 | isDarkTheme: boolean, 24 | setMyDeviceDetails: (details: DeviceDetails) => void, 25 | hostAllowedToConnectCallback: () => void, 26 | setScreenSharingSourceTypeCallback: (s: ScreenSharingSourceType) => void, 27 | setIsDarkThemeCallback: (val: boolean) => void, 28 | setAppLanguageCallback: (newLang: string) => void, 29 | setDialogErrorMessageCallback: (message: ErrorMessage) => void, 30 | setIsErrorDialogOpen: (val: boolean) => void 31 | ) { 32 | this.isDarkTheme = isDarkTheme; 33 | this.hostAllowedToConnectCallback = hostAllowedToConnectCallback; 34 | this.setMyDeviceDetails = setMyDeviceDetails; 35 | this.setScreenSharingSourceTypeCallback = setScreenSharingSourceTypeCallback; 36 | this.setIsDarkThemeCallback = setIsDarkThemeCallback; 37 | this.setAppLanguageCallback = setAppLanguageCallback; 38 | this.setDialogErrorMessageCallback = setDialogErrorMessageCallback; 39 | this.setIsErrorDialogOpen = setIsErrorDialogOpen; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/ReceiveEncryptedMessagePayload.d.ts: -------------------------------------------------------------------------------- 1 | interface ReceiveEncryptedMessagePayload { 2 | fromSocketID: string; 3 | payload: string; 4 | signature: string; 5 | iv: string; 6 | keys: { sessionKey: string; signingKey: string }[]; 7 | } 8 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/ScreenSharingSourceEnum.ts: -------------------------------------------------------------------------------- 1 | enum ScreenSharingSource { 2 | WINDOW = 'window', 3 | SCREEN = 'screen', 4 | } 5 | 6 | export default ScreenSharingSource; 7 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/errors/PeerConnectionPartnerIsNotDefinedError.ts: -------------------------------------------------------------------------------- 1 | export default class PeerConnectionPartnerIsNotDefinedError extends Error { 2 | constructor() { 3 | super('partner should be defined!'); 4 | // Set the prototype explicitly. 5 | Object.setPrototypeOf(this, PeerConnectionPartnerIsNotDefinedError.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/errors/PeerConnectionPeerIsNullError.ts: -------------------------------------------------------------------------------- 1 | export default class PeerConnectionPeerIsNullError extends Error { 2 | constructor() { 3 | super('peer of PeerConnection should not be null!'); 4 | // Set the prototype explicitly. 5 | Object.setPrototypeOf(this, PeerConnectionPeerIsNullError.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/errors/PeerConnectionSocketNotDefined.ts: -------------------------------------------------------------------------------- 1 | export default class PeerConnectionSocketNotDefined extends Error { 2 | constructor() { 3 | super('socket should be defined!'); 4 | // Set the prototype explicitly. 5 | Object.setPrototypeOf(this, PeerConnectionSocketNotDefined.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/errors/PeerConnectionUserIsNotDefinedError.ts: -------------------------------------------------------------------------------- 1 | export default class PeerConnectionUserIsNotDefinedError extends Error { 2 | constructor() { 3 | super('user should be defined!'); 4 | // Set the prototype explicitly. 5 | Object.setPrototypeOf(this, PeerConnectionUserIsNotDefinedError.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/mocks/INPUTtestWindowNavigatorUserAgent.ts: -------------------------------------------------------------------------------- 1 | export const INPUTtestWindowNavigatorUserAgent = 2 | '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'; 3 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/mocks/OUTPUTDeviceDetailsFromUAParsed.ts: -------------------------------------------------------------------------------- 1 | export const OUTPUTDeviceDetailsFromUAParsed: DeviceDetails = { 2 | myBrowser: 'Chrome 87.0.4280.88', 3 | myDeviceType: 'computer', 4 | myIP: '123.123.123.123', 5 | myOS: 'Mac OS 10.15.6', 6 | }; 7 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/peerConnectionReceiveEncryptedMessage.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum'; 2 | import { process as processMessage } from '../../utils/message'; 3 | import NullUser from './NullUser'; 4 | import PeerConnectionUserIsNotDefinedError from './errors/PeerConnectionUserIsNotDefinedError'; 5 | import setAndShowErrorDialogMessage from './setAndShowErrorDialogMessage'; 6 | 7 | export default async ( 8 | peerConnection: PeerConnection, 9 | payload: ReceiveEncryptedMessagePayload 10 | ) => { 11 | if (peerConnection.user === NullUser) { 12 | throw new PeerConnectionUserIsNotDefinedError(); 13 | } 14 | const message = (await processMessage( 15 | payload, 16 | peerConnection.user.privateKey 17 | )) as any; 18 | if (message.type === 'CALL_USER') { 19 | peerConnection.peer?.signal(message.payload.signalData); 20 | } 21 | if (message.type === 'DENY_TO_CONNECT') { 22 | setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT); 23 | } 24 | if (message.type === 'DISCONNECT_BY_HOST_MACHINE_USER') { 25 | setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DISCONNECTED); 26 | } 27 | if (message.type === 'ALLOWED_TO_CONNECT') { 28 | peerConnection.UIHandler.hostAllowedToConnectCallback(); 29 | } 30 | if (message.type === 'APP_THEME') { 31 | if (peerConnection.UIHandler.isDarkTheme !== message.payload.value) { 32 | peerConnection.UIHandler.setIsDarkThemeCallback(message.payload.value); 33 | peerConnection.UIHandler.isDarkTheme = message.payload.value; 34 | } 35 | } 36 | if (message.type === 'APP_LANGUAGE') { 37 | peerConnection.UIHandler.setAppLanguageCallback(message.payload.value); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/setAndShowErrorDialogMessage.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage } from '../../components/ErrorDialog/ErrorMessageEnum'; 2 | 3 | export default (peerConnection: PeerConnection, errorMessage: ErrorMessage) => { 4 | if ( 5 | peerConnection.UIHandler.errorDialogMessage === ErrorMessage.UNKNOWN_ERROR 6 | ) { 7 | peerConnection.UIHandler.setDialogErrorMessageCallback(errorMessage); 8 | peerConnection.UIHandler.setIsErrorDialogOpen(true); 9 | peerConnection.UIHandler.errorDialogMessage = errorMessage; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/setSdpMediaBitrate.spec.ts: -------------------------------------------------------------------------------- 1 | import { INPUTtestSdpMediaBitrate } from './mocks/INPUTvideo500000testSdpMediaBitrate'; 2 | import { OUTPUTtestSdpMediaBitrate } from './mocks/OUTPUTvideo500000testSdpMediaBitrate'; 3 | import setSdpMediaBitrate from './setSdpMediaBitrate'; 4 | 5 | describe('setSdpMediaBitrate', () => { 6 | describe('when setSdpMediaBitrate is called with sdp input', () => { 7 | it('should produce a result that should match with test sdp string', () => { 8 | const result = setSdpMediaBitrate( 9 | INPUTtestSdpMediaBitrate, 10 | 'video', 11 | 500000 12 | ); 13 | 14 | expect(result).toMatch(OUTPUTtestSdpMediaBitrate); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/setSdpMediaBitrate.ts: -------------------------------------------------------------------------------- 1 | export default (sdp: string, mediaType: string, bitrate: number) => { 2 | const sdpLines = sdp.split('\n'); 3 | let mediaLineIndex = -1; 4 | const mediaLine = `m=${mediaType}`; 5 | let bitrateLineIndex = -1; 6 | const bitrateLine = `b=AS:${bitrate}`; 7 | mediaLineIndex = sdpLines.findIndex((line) => line.startsWith(mediaLine)); 8 | 9 | // If we find a line matching “m={mediaType}” 10 | if (mediaLineIndex && mediaLineIndex < sdpLines.length) { 11 | // Skip the media line 12 | bitrateLineIndex = mediaLineIndex + 1; 13 | 14 | // Skip both i=* and c=* lines (bandwidths limiters have to come afterwards) 15 | while ( 16 | sdpLines[bitrateLineIndex].startsWith('i=') || 17 | sdpLines[bitrateLineIndex].startsWith('c=') 18 | ) { 19 | bitrateLineIndex += 1; 20 | } 21 | 22 | if (sdpLines[bitrateLineIndex].startsWith('b=')) { 23 | // If the next line is a b=* line, replace it with our new bandwidth 24 | sdpLines[bitrateLineIndex] = bitrateLine; 25 | } else { 26 | // Otherwise insert a new bitrate line. 27 | sdpLines.splice(bitrateLineIndex, 0, bitrateLine); 28 | } 29 | } 30 | 31 | // Then return the updated sdp content as a string 32 | return sdpLines.join('\n'); 33 | }; 34 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/simplePeerDataMessages.ts: -------------------------------------------------------------------------------- 1 | export function prepareDataMessageToChangeQuality(q: number) { 2 | return ` 3 | { 4 | "type": "set_video_quality", 5 | "payload": { 6 | "value": ${q} 7 | } 8 | } 9 | `; 10 | } 11 | 12 | export function prepareDataMessageToGetSharingSourceType(){ 13 | return ` 14 | { 15 | "type": "get_sharing_source_type", 16 | "payload": { 17 | } 18 | } 19 | `; 20 | } 21 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/startSocketConnectedCheckingLoop/index.spec.ts: -------------------------------------------------------------------------------- 1 | import PeerConnection from '..'; 2 | import VideoAutoQualityOptimizer from '../../VideoAutoQualityOptimizer'; 3 | import Crypto from '../../../utils/crypto'; 4 | import startSocketConnectedCheckingLoop from '.'; 5 | import setAndShowErrorDialogMessage from '../setAndShowErrorDialogMessage'; 6 | import PeerConnectionUIHandler from '../PeerConnectionUIHandler'; 7 | 8 | jest.useFakeTimers(); 9 | 10 | jest.mock('../setAndShowErrorDialogMessage'); 11 | 12 | describe('startSocketConnectedCheckingLoop', () => { 13 | let peerConnection: PeerConnection; 14 | 15 | beforeEach(() => { 16 | jest.clearAllMocks(); 17 | 18 | peerConnection = new PeerConnection( 19 | '123', 20 | jest.fn(), 21 | new Crypto(), 22 | new VideoAutoQualityOptimizer(), 23 | new PeerConnectionUIHandler( 24 | true, 25 | jest.fn(), 26 | jest.fn(), 27 | jest.fn(), 28 | jest.fn(), 29 | jest.fn(), 30 | jest.fn(), 31 | jest.fn() 32 | ) 33 | ); 34 | 35 | peerConnection.socket = { connected: true }; 36 | }); 37 | 38 | afterEach(() => { 39 | jest.restoreAllMocks(); 40 | jest.clearAllMocks(); 41 | }); 42 | 43 | describe('when interval passed and socket is connected', () => { 44 | it('should NOT call setAndShowErrorDialogMessage', () => { 45 | startSocketConnectedCheckingLoop(peerConnection); 46 | jest.advanceTimersByTime(3000); 47 | 48 | expect(setAndShowErrorDialogMessage).not.toBeCalled(); 49 | }); 50 | }); 51 | 52 | describe('when interval passed and socket is NOT connected', () => { 53 | it('should call setAndShowErrorDialogMessage', () => { 54 | peerConnection.socket.connected = false; 55 | 56 | startSocketConnectedCheckingLoop(peerConnection); 57 | jest.advanceTimersByTime(3000); 58 | 59 | expect(setAndShowErrorDialogMessage).toBeCalled(); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /app/client/src/features/PeerConnection/startSocketConnectedCheckingLoop/index.ts: -------------------------------------------------------------------------------- 1 | import PeerConnection from '..'; 2 | import { ErrorMessage } from '../../../components/ErrorDialog/ErrorMessageEnum'; 3 | import setAndShowErrorDialogMessage from '../setAndShowErrorDialogMessage'; 4 | 5 | export default (peerConnection: PeerConnection) => { 6 | setInterval(() => { 7 | if (!peerConnection.socket.connected) { 8 | setAndShowErrorDialogMessage(peerConnection, ErrorMessage.DENY_TO_CONNECT); 9 | } 10 | }, 2000); 11 | }; 12 | -------------------------------------------------------------------------------- /app/client/src/features/VideoAutoQualityOptimizer/VideoQualityEnum.ts: -------------------------------------------------------------------------------- 1 | export enum VideoQuality { 2 | Q_AUTO = 'Auto', 3 | Q_25_PERCENT = '25%', 4 | Q_40_PERCENT = '40%', 5 | Q_60_PERCENT = '60%', 6 | Q_80_PERCENT = '80%', 7 | Q_100_PERCENT = '100%', 8 | } 9 | -------------------------------------------------------------------------------- /app/client/src/features/VideoAutoQualityOptimizer/errors/CanvasNotDefinedError.ts: -------------------------------------------------------------------------------- 1 | export default class CanvasNotDefinedError extends Error { 2 | constructor() { 3 | super('internal variable of canvas DOM element should be defined!'); 4 | // Set the prototype explicitly. 5 | Object.setPrototypeOf(this, CanvasNotDefinedError.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/client/src/features/VideoAutoQualityOptimizer/errors/ImageDataIsUndefinedError.ts: -------------------------------------------------------------------------------- 1 | export default class ImageDataIsUndefinedError extends Error { 2 | constructor() { 3 | super('imageData retrieved is undefined!'); 4 | // Set the prototype explicitly. 5 | Object.setPrototypeOf(this, ImageDataIsUndefinedError.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/client/src/features/VideoAutoQualityOptimizer/errors/VideoDimensionsAreWrongError.ts: -------------------------------------------------------------------------------- 1 | export default class VideoDimensionsAreWrongError extends Error { 2 | constructor() { 3 | super( 4 | 'video dimensions are wrong, neither width nor height can be zero!' 5 | ); 6 | // Set the prototype explicitly. 7 | Object.setPrototypeOf(this, VideoDimensionsAreWrongError.prototype); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/client/src/features/VideoAutoQualityOptimizer/errors/VideoNotDefinedError.ts: -------------------------------------------------------------------------------- 1 | export default class VideoNotDefinedError extends Error { 2 | constructor() { 3 | super('internal variable of video DOM element should be defined!'); 4 | // Set the prototype explicitly. 5 | Object.setPrototypeOf(this, VideoNotDefinedError.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/client/src/images/deskreen_logo_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavlobu/deskreen/4a87fc16d4c448cf9936a63fd84c1712bb5016ee/app/client/src/images/deskreen_logo_128x128.png -------------------------------------------------------------------------------- /app/client/src/images/fullscreen_24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/src/images/fullscreen_exit-24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/client/src/images/red_heart_2764_twemoji_120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavlobu/deskreen/4a87fc16d4c448cf9936a63fd84c1712bb5016ee/app/client/src/images/red_heart_2764_twemoji_120x120.png -------------------------------------------------------------------------------- /app/client/src/index.css: -------------------------------------------------------------------------------- 1 | @import "~normalize.css"; 2 | @import "~@blueprintjs/core/lib/css/blueprint.css"; 3 | @import "~@blueprintjs/icons/lib/css/blueprint-icons.css"; 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | -------------------------------------------------------------------------------- /app/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | import React, { Suspense } from 'react'; 3 | import './config/i18n'; 4 | import ReactDOM from 'react-dom'; 5 | import './index.css'; 6 | import App from './App'; 7 | import * as serviceWorker from './serviceWorker'; 8 | import { AppContextProvider } from './providers/AppContextProvider'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById('root') 19 | ); 20 | 21 | // If you want your app to work offline and load faster, you can change 22 | // unregister() to register() below. Note this comes with some pitfalls. 23 | // Learn more about service workers: https://bit.ly/CRA-PWA 24 | serviceWorker.unregister(); 25 | -------------------------------------------------------------------------------- /app/client/src/polyfills.tsx: -------------------------------------------------------------------------------- 1 | // recommended by create-react-app 2 | import 'react-app-polyfill/ie9'; 3 | import 'react-app-polyfill/ie11'; 4 | import 'react-app-polyfill/stable'; 5 | // probably is better to load polyfills on demand, 6 | // browser tests are required to detect which polyfills are needed 7 | import 'core-js'; 8 | -------------------------------------------------------------------------------- /app/client/src/providers/AppContextProvider/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import { Classes } from '@blueprintjs/core'; 3 | import React, { useState } from 'react'; 4 | import { 5 | DARK_UI_BACKGROUND, 6 | LIGHT_UI_BACKGROUND, 7 | } from '../../constants/styleConstants'; 8 | 9 | interface AppContextInterface { 10 | isDarkTheme: boolean; 11 | setIsDarkThemeHook: (val: boolean) => void; 12 | } 13 | 14 | const defaultAppContextValue = { 15 | isDarkTheme: false, 16 | setIsDarkThemeHook: () => {}, 17 | }; 18 | 19 | export const AppContext = React.createContext( 20 | defaultAppContextValue 21 | ); 22 | 23 | export const AppContextProvider: React.FC = ({ children }) => { 24 | const [isDarkTheme, setIsDarkTheme] = useState(false); 25 | const [appLanguage, setAppLanguage] = useState('en'); 26 | 27 | const setIsDarkThemeHook = (val: boolean) => { 28 | document.body.classList.toggle(Classes.DARK); 29 | 30 | document.body.style.backgroundColor = val 31 | ? DARK_UI_BACKGROUND 32 | : LIGHT_UI_BACKGROUND; 33 | setIsDarkTheme(val); 34 | }; 35 | 36 | const setAppLanguageHook = (newLang: string) => { 37 | setAppLanguage(newLang); 38 | }; 39 | 40 | const value = { 41 | isDarkTheme, 42 | setIsDarkThemeHook, 43 | appLanguage, 44 | setAppLanguageHook, 45 | }; 46 | 47 | return {children}; 48 | }; 49 | -------------------------------------------------------------------------------- /app/client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | const ConnectionIconEnum = import('./containers/MainView/ConnectionIconEnum').default; 4 | const LoadingSharingIconEnum = import('./containers/MainView/LoadingSharingIconEnum').default; 5 | const ScreenSharingSourceEnum = import('./features/PeerConnection/ScreenSharingSourceEnum').default; 6 | 7 | type ConnectionIconType = ConnectionIconEnum.FEED | ConnectionIconEnum.FEED_SUBSCRIBED; 8 | type LoadingSharingIconType = LoadingSharingIconEnum.DESKTOP | LoadingSharingIconEnum.APPLICATION; 9 | type ScreenSharingSourceType = 10 | | ScreenSharingSourceEnum.SCREEN 11 | | ScreenSharingSourceEnum.WINDOW; 12 | type CreatePeerConnectionUseEffectParams = { 13 | isDarkTheme: boolean; 14 | peer: undefined | PeerConnection; 15 | setIsDarkThemeHook: (_: boolean) => void; 16 | setMyDeviceDetails: (_: DeviceDetails) => void; 17 | setConnectionIconType: (_: ConnectionIconType) => void; 18 | setIsShownTextPrompt: (_: boolean) => void; 19 | setPromptStep: (_: number) => void; 20 | setScreenSharingSourceType: (_: ScreenSharingSourceType) => void; 21 | setDialogErrorMessage: (_: ErrorMessage) => void; 22 | setIsErrorDialogOpen: (_: boolean) => void; 23 | setUrl: (_: MediaStream) => void; 24 | setPeer: (_: PeerConnection) => void; 25 | }; 26 | type handleDisplayingLoadingSharingIconLoopParams = { 27 | promptStep: number; 28 | url: undefined | MediaStream; 29 | setIsShownLoadingSharingIcon: (_: boolean) => void; 30 | loadingSharingIconType: LoadingSharingIconType; 31 | isShownLoadingSharingIcon: boolean; 32 | setLoadingSharingIconType: (_: LoadingSharingIconType) => void; 33 | }; 34 | -------------------------------------------------------------------------------- /app/client/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /app/client/src/utils/ProcessedMessage.d.ts: -------------------------------------------------------------------------------- 1 | type CallUserMessageWithPayload = { 2 | type: 'CALL_USER'; 3 | payload: { 4 | signalData: string; 5 | }; 6 | }; 7 | 8 | type DeviceDetailsMessageWithPayload = { 9 | type: 'DEVICE_DETAILS'; 10 | payload: { 11 | socketID: string; 12 | deviceType: { type: string }; 13 | os: { name: string; version: string }; 14 | browser: { name: string; version: string; major: string }; 15 | deviceScreenWidth: number; 16 | deviceScreenHeight: number; 17 | }; 18 | }; 19 | 20 | type DenyToConnectMessageWithPayload = { 21 | type: 'DENY_TO_CONNECT'; 22 | payload: {}; 23 | }; 24 | 25 | type DisconnectByHostMachineUserMessageWithPayload = { 26 | type: 'DISCONNECT_BY_HOST_MACHINE_USER'; 27 | payload: {}; 28 | }; 29 | 30 | type AllowedToConnectMessageWithPayload = { 31 | type: 'ALLOWED_TO_CONNECT'; 32 | payload: {}; 33 | }; 34 | 35 | type AppThemeMessageWithPayload = { 36 | type: 'APP_THEME'; 37 | payload: { 38 | value: boolean, 39 | }; 40 | }; 41 | 42 | type AppLanguageMessageWithPayload = { 43 | type: 'APP_LANGUAGE'; 44 | payload: { 45 | value: string, 46 | }; 47 | }; 48 | 49 | 50 | type ProcessedMessage = 51 | | CallUserMessageWithPayload 52 | | DeviceDetailsMessageWithPayload 53 | | DenyToConnectMessageWithPayload 54 | | DisconnectByHostMachineUserMessageWithPayload 55 | | AllowedToConnectMessageWithPayload 56 | | AppThemeMessageWithPayload 57 | | AppLanguageMessageWithPayload; 58 | -------------------------------------------------------------------------------- /app/client/src/utils/areWeTestingWithJest.ts: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return process.env.JEST_WORKER_ID !== undefined; 3 | }; 4 | -------------------------------------------------------------------------------- /app/client/src/utils/getRoomIDOfCurrentBrowserWindow.ts: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return encodeURI(window.location.pathname.replace('/', '')); 3 | } 4 | -------------------------------------------------------------------------------- /app/client/src/utils/message.spec.ts: -------------------------------------------------------------------------------- 1 | import { prepare, process } from './message'; 2 | import getTestPublickKeyPem from './mocks/getTestPublicKeyPem'; 3 | import getTestPrivateKeyPem from './mocks/getTestPrivateKeyPem'; 4 | 5 | interface TestPayload { 6 | text: string; 7 | } 8 | 9 | describe('message.ts tests for proper encryption and decryption functionality', () => { 10 | const TEST_TEXT = 'some test text here'; 11 | const TEST_TEXT_AS_URL = 'some%20test%20text%20here'; 12 | const testPayloadToEncrypt = { 13 | payload: { 14 | text: TEST_TEXT, 15 | }, 16 | }; 17 | 18 | const testUser = { 19 | username: 'testUsername', 20 | id: 'testId', 21 | }; 22 | 23 | const testPartner = { 24 | publicKey: getTestPublickKeyPem(), 25 | }; 26 | it('should create encrypted payload with prepare() method', async () => { 27 | const encryptedPayload = await prepare( 28 | testPayloadToEncrypt, 29 | testUser, 30 | testPartner 31 | ); 32 | 33 | expect(encryptedPayload.toSend.payload).not.toContain(TEST_TEXT); 34 | expect(encryptedPayload.toSend.payload).not.toContain(TEST_TEXT_AS_URL); 35 | }); 36 | 37 | it('should decrypt encrypted payload with process() method', async () => { 38 | const encryptedPayload = await prepare( 39 | testPayloadToEncrypt, 40 | testUser, 41 | testPartner 42 | ); 43 | 44 | const decryptedPayload = await process( 45 | encryptedPayload.toSend, 46 | getTestPrivateKeyPem() 47 | ); 48 | 49 | expect( 50 | ((decryptedPayload.payload as unknown) as TestPayload).text 51 | ).toContain(TEST_TEXT_AS_URL); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /app/client/src/utils/mocks/getTestPrivateKeyPem.ts: -------------------------------------------------------------------------------- 1 | export default function getTestPrivateKeyPem() { 2 | return `-----BEGIN RSA PRIVATE KEY----- 3 | MIIEogIBAAKCAQEAoQ8+vdVAerEyaxGaffzrTTv+sgpQ3xToBFYkxrT6f+zP4MqV 4 | nTIy2+UMlEGGryMFgJWyurqv+NyoVf7vIFOxQcxJko1BL/oIt/e5YZ/fMIw9AhgP 5 | 0A0oZyFKNbGesCY3zMdpZqPE0brzfhjr/lu5VzioZI9vxocOSSc3+S8w1EXqujgO 6 | X3PWXgZrD6Y//Oo+8BgNQta/e5PUyc9yNchU/W3ddzBdE0iUXTdQt7yFvzy4vTbS 7 | ywUxoNNJnD6dUJ6dZ4c24VGvrvhJ5mzNqQjibAhtkPFbvhn0e0BHl0BZ876fLHGg 8 | zpHgmmiWbKwBYl1ydv8fW9W3r5nQoW0RThYJjwIDAQABAoIBAHqOrUGrGsvCNwl+ 9 | db9VTICTHLbCXtPChuN14bpLUSszOuRlhAAAiO8HltDiI+j1j2RPhZfOI8YNsxLt 10 | UW2aAhJ9r6aTUn19mFDVcv20uBOrQ2lqge3hdVM049GD/asxCdkMDUqLaGPoDQ1x 11 | TXNavOiANrN+6qF5eAd2joNRw6hi7osyWqfpgM9y58kiYYazKHKlI/er19JsD2t5 12 | hgRiFti+oN9nqhhVzJI/GYU9JugXnB/Z0uFvqOyt/3YDHPpOC07WYpfA3yzshBkW 13 | TiiMp1eJNX/sRh4JHzRI9/MQe9ajlAS7T74HwCkclzaS8ZY6t+dfVZ097/ug77eQ 14 | NwY4DAECgYEA0hWv7hpookKKcl0srfxbx+FbtBhzXqz+Tfy3H5do6uXRc0aUwQ25 15 | PZLs7UAT3zjt+4aYu1xgkp3cUx5FNr/FAPYLtjXw1/6fmt3HX8820SpCMjIzS1bU 16 | UwJvmAut70YA+n2eKwO1qJNeLcpNCDebbIXwj0lKDh5HQDyoswuz7I8CgYEAxEKV 17 | N4Ma6GqZ0hgdQlO71oSuftHUI80Iz+Riorrp4AA8hp5A3V6Fd9QDy8PYLz1kD9pM 18 | uWdCLqaxdOVDtLVnjNVZH1SI+wSBDA/0OtdvPaONL1JKRA21kol049f0M8wkulAw 19 | 6lfi2aQwZ3KWyEAfDy5iPdVROnQWqUcLmxO0kwECgYAGpSr4dBtlLoekkG/uXPIm 20 | Q2mcK73Se9RbcSf1tttZusVCSTRBWwbF/NTDuGgogmt8rkg8fPKNELM8adO0pKI9 21 | oorCS7h/jI1N38ADttE8EoMfhVj8BBYZPhV7kLsCu4siYUDUiXyAhZDQD/sZzHB9 22 | IUt3rNDL24dTb9fCOheJ3wKBgGRgpY7Z2DZM51VkDfrxdp3WCKVGTljtMfeaGLSg 23 | IqP1mv9DC2vtPxg1cKeUCArJPFc7UIh2/ot7qEFgTQusyERojgePJew0tofj1QcP 24 | To7ZConMbb12wYosEYPC3NxtKc+82ffRcW3dIwCVw/axjPEnyQlVBBGAdGKpuo7b 25 | Oj0BAoGAMs2zuAJfJB4xE1UZdLXk9uHWPAzgUNDzHuS5mMWuloGvKX+19yck6o2J 26 | ZA/iq6GwOgvD2y9MC0mV4WkmwZpVXwPVpZD8GVQXZf2xZgh/q2e3IObAd80NLnaG 27 | v86qN98DRTh9L+47Nsaf1J5vDDaAfH2Ir8UgAQ5ZMEFDm7P0hUQ= 28 | -----END RSA PRIVATE KEY-----`; 29 | } 30 | -------------------------------------------------------------------------------- /app/client/src/utils/mocks/getTestPublicKeyPem.ts: -------------------------------------------------------------------------------- 1 | export default function getTestPublicKeyPem() { 2 | return `-----BEGIN PUBLIC KEY----- 3 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoQ8+vdVAerEyaxGaffzr 4 | TTv+sgpQ3xToBFYkxrT6f+zP4MqVnTIy2+UMlEGGryMFgJWyurqv+NyoVf7vIFOx 5 | QcxJko1BL/oIt/e5YZ/fMIw9AhgP0A0oZyFKNbGesCY3zMdpZqPE0brzfhjr/lu5 6 | VzioZI9vxocOSSc3+S8w1EXqujgOX3PWXgZrD6Y//Oo+8BgNQta/e5PUyc9yNchU 7 | /W3ddzBdE0iUXTdQt7yFvzy4vTbSywUxoNNJnD6dUJ6dZ4c24VGvrvhJ5mzNqQji 8 | bAhtkPFbvhn0e0BHl0BZ876fLHGgzpHgmmiWbKwBYl1ydv8fW9W3r5nQoW0RThYJ 9 | jwIDAQAB 10 | -----END PUBLIC KEY-----`; 11 | } 12 | -------------------------------------------------------------------------------- /app/client/src/utils/socket.ts: -------------------------------------------------------------------------------- 1 | import socketIO from 'socket.io-client'; 2 | import generateUrl from '../api/generator'; 3 | 4 | let socket: SocketIOClient.Socket; 5 | 6 | export const connect = (roomId: string) => { 7 | socket = socketIO(generateUrl(), { 8 | query: { 9 | roomId, 10 | }, 11 | forceNew: true, 12 | }); 13 | return socket; 14 | }; 15 | 16 | export const getSocket = () => socket; 17 | -------------------------------------------------------------------------------- /app/client/src/utils/userAgentParserHelpers.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { UAParser } from 'ua-parser-js'; 3 | 4 | export function getOSFromUAParser(uaParser: UAParser) { 5 | const osFromUAParser = uaParser.getResult().os; 6 | 7 | return `${osFromUAParser.name ? osFromUAParser.name : ''} ${ 8 | osFromUAParser.version ? osFromUAParser.version : '' 9 | }`; 10 | } 11 | 12 | export function getDeviceTypeFromUAParser(uaParser: UAParser) { 13 | const deviceTypeFromUAParser = uaParser.getResult().device; 14 | 15 | return deviceTypeFromUAParser.type 16 | ? deviceTypeFromUAParser.type.toString() 17 | : 'computer' 18 | } 19 | 20 | export function getBrowserFromUAParser(uaParser: UAParser) { 21 | const browserFromUAParser = uaParser.getResult().browser; 22 | 23 | return `${browserFromUAParser.name ? browserFromUAParser.name : ''} ${ 24 | browserFromUAParser.version ? browserFromUAParser.version : '' 25 | }`; 26 | } 27 | -------------------------------------------------------------------------------- /app/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "react", 20 | "strict": true, 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /app/components/AllowConnectionForDeviceAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Intent, Alert, H4 } from '@blueprintjs/core'; 3 | import { useTranslation } from 'react-i18next'; 4 | import DeviceInfoCallout from './DeviceInfoCallout'; 5 | import isWithReactRevealAnimations from '../utils/isWithReactRevealAnimations'; 6 | 7 | interface AllowConnectionForDeviceAlertProps { 8 | device: Device | null; 9 | isOpen: boolean; 10 | onCancel: () => void; 11 | onConfirm: () => void; 12 | } 13 | 14 | export default function AllowConnectionForDeviceAlert( 15 | props: AllowConnectionForDeviceAlertProps 16 | ) { 17 | const { t } = useTranslation(); 18 | const { device, isOpen, onCancel, onConfirm } = props; 19 | const denyText = t('Deny'); 20 | const allowText = t('Allow'); 21 | 22 | return ( 23 | 37 |

{t('Someone is trying to connect, do you allow?')}

38 | 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /app/components/CloseOverlayButton.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/destructuring-assignment */ 2 | import React from 'react'; 3 | import { createStyles, makeStyles } from '@material-ui/core/styles'; 4 | import { Button, Icon } from '@blueprintjs/core'; 5 | 6 | class CloseOverlayButtonProps { 7 | onClick = () => {}; 8 | 9 | style? = {}; 10 | 11 | isDefaultStyles? = false; 12 | 13 | className? = ''; 14 | } 15 | 16 | const useStyles = makeStyles(() => 17 | createStyles({ 18 | closeButton: { 19 | position: 'relative', 20 | width: '40px', 21 | height: '40px', 22 | left: 'calc(100% - 55px)', 23 | borderRadius: '100px', 24 | zIndex: 9999, 25 | }, 26 | }) 27 | ); 28 | 29 | const CloseOverlayButton: React.FC = ( 30 | props: CloseOverlayButtonProps 31 | ) => { 32 | const { className, isDefaultStyles, style, onClick } = props; 33 | const classes = useStyles(); 34 | return ( 35 | 43 | ); 44 | }; 45 | 46 | export default CloseOverlayButton; 47 | -------------------------------------------------------------------------------- /app/components/LanguageSelector/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react'; 2 | import { ipcRenderer } from 'electron'; 3 | import { HTMLSelect } from '@blueprintjs/core'; 4 | import i18n from 'i18next'; 5 | import { SettingsContext } from '../../containers/SettingsProvider'; 6 | import i18n_client, { 7 | getLangFullNameToLangISOKeyMap, 8 | getLangISOKeyToLangFullNameMap, 9 | } from '../../configs/i18next.config.client'; 10 | import { IpcEvents } from '../../main/IpcEvents.enum'; 11 | 12 | export default function LanguageSelector() { 13 | const { setCurrentLanguageHook } = useContext(SettingsContext); 14 | 15 | const [languagesList, setLanguagesList] = useState([] as string[]); 16 | 17 | useEffect(() => { 18 | const tmp: string[] = []; 19 | getLangFullNameToLangISOKeyMap().forEach((_, key) => { 20 | tmp.push(key); 21 | }); 22 | setLanguagesList(tmp); 23 | setCurrentLanguageHook(i18n_client.language); 24 | }, [setCurrentLanguageHook]); 25 | 26 | const onChangeLanguageHTMLSelectHandler = ( 27 | event: React.ChangeEvent 28 | ) => { 29 | if ( 30 | event.currentTarget && 31 | getLangFullNameToLangISOKeyMap().has(event.currentTarget.value) 32 | ) { 33 | const newLang = 34 | getLangFullNameToLangISOKeyMap().get(event.currentTarget.value) || 35 | 'English'; 36 | i18n.changeLanguage(newLang); 37 | ipcRenderer.invoke(IpcEvents.AppLanguageChanged, newLang); 38 | } 39 | }; 40 | 41 | return ( 42 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/components/SettingsOverlay/SettingRowLabelAndInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Row, Col } from 'react-flexbox-grid'; 3 | import { Icon, Text } from '@blueprintjs/core'; 4 | import { createStyles, makeStyles } from '@material-ui/core/styles'; 5 | import { SettingsContext } from '../../containers/SettingsProvider'; 6 | 7 | const useStylesWithTheme = (isDarkTheme: boolean) => 8 | makeStyles(() => 9 | createStyles({ 10 | oneSettingRow: { 11 | color: isDarkTheme ? '#CED9E0 !important' : '#5C7080 !important', 12 | fontSize: '18px', 13 | fontWeight: 900, 14 | }, 15 | settingRowIcon: { 16 | margin: '10px', 17 | color: isDarkTheme ? '#BFCCD6' : '#8A9BA8', 18 | }, 19 | }) 20 | ); 21 | 22 | interface SettingRowLabelAndInput { 23 | icon: string; 24 | label: string; 25 | input: React.ReactNode; 26 | } 27 | 28 | export default function SettingRowLabelAndInput( 29 | props: SettingRowLabelAndInput 30 | ) { 31 | const { icon, label, input } = props; 32 | const { isDarkTheme } = useContext(SettingsContext); 33 | 34 | const getClassesCallback = useStylesWithTheme(isDarkTheme); 35 | 36 | return ( 37 | 38 | 39 | 40 | 41 | 48 | 49 | 50 | {label} 51 | 52 | 53 | 54 | 55 | {input} 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /app/components/StepperPanel/ColorlibConnector.tsx: -------------------------------------------------------------------------------- 1 | import { withStyles } from '@material-ui/core/styles'; 2 | import StepConnector from '@material-ui/core/StepConnector'; 3 | 4 | const ColorlibConnector = withStyles({ 5 | alternativeLabel: { 6 | top: 43, 7 | }, 8 | active: { 9 | '& $line': { 10 | backgroundImage: 11 | 'linear-gradient( 95deg, #3DCC91 0%, #15B371 50%, #FFB366 100%)', 12 | }, 13 | }, 14 | completed: { 15 | '& $line': { 16 | backgroundImage: 17 | 'linear-gradient( 95deg, #3DCC91 0%, #15B371 50%, #3DCC91 100%)', 18 | }, 19 | }, 20 | line: { 21 | height: 2, 22 | border: 0, 23 | backgroundColor: '#CED9E0', 24 | borderRadius: 1, 25 | }, 26 | })(StepConnector); 27 | 28 | export default ColorlibConnector; 29 | -------------------------------------------------------------------------------- /app/components/StepperPanel/ColorlibStepIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import { StepIconProps } from '@material-ui/core/StepIcon'; 5 | import { Icon } from '@blueprintjs/core'; 6 | 7 | export interface StepIconPropsDeskreen extends StepIconProps { 8 | isEntireScreenSelected: boolean; 9 | isApplicationWindowSelected: boolean; 10 | } 11 | 12 | const useColorlibStepIconStyles = makeStyles({ 13 | root: { 14 | backgroundColor: '#BFCCD6', 15 | zIndex: 1, 16 | color: '#5C7080', 17 | width: 65, 18 | height: 65, 19 | display: 'flex', 20 | borderRadius: '50%', 21 | justifyContent: 'center', 22 | alignItems: 'center', 23 | }, 24 | active: { 25 | backgroundImage: 26 | 'linear-gradient( 136deg, #FFB366 0%, #F29D49 50%, #A66321 100%)', 27 | boxShadow: '0 4px 10px 0 rgba(0,0,0,.25)', 28 | }, 29 | completed: { 30 | backgroundImage: 31 | 'linear-gradient( 136deg, #3DCC91 0%, #15B371 50%, #0E5A8A 100%)', 32 | }, 33 | stepContent: {}, 34 | }); 35 | 36 | const getDesktopOrAppIcon = (isDesktop: boolean, color: string) => { 37 | if (isDesktop) { 38 | return ; 39 | } 40 | return ; 41 | }; 42 | 43 | export default function ColorlibStepIcon(props: StepIconPropsDeskreen) { 44 | const { icon } = props; 45 | const classes = useColorlibStepIconStyles(); 46 | const { active, completed, isEntireScreenSelected } = props; 47 | 48 | const color = active || completed ? '#fff' : '#5C7080'; 49 | 50 | const icons: { [index: string]: React.ReactElement } = { 51 | 1: completed ? ( 52 | 53 | ) : ( 54 | 55 | ), 56 | 2: completed ? ( 57 | getDesktopOrAppIcon(isEntireScreenSelected, color) 58 | ) : ( 59 | 60 | ), 61 | 3: completed ? ( 62 | 63 | ) : ( 64 | 65 | ), 66 | }; 67 | 68 | return ( 69 |
75 | {icons[String(icon)]} 76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /app/components/StepsOfStepper/ChooseAppOrScreeenStep.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col } from 'react-flexbox-grid'; 3 | import ShareEntireScreenOrAppWindowControlGroup from '../ShareAppOrScreenControlGroup'; 4 | 5 | interface ChooseAppOrScreeenStepProps { 6 | handleNextEntireScreen: () => void; 7 | handleNextApplicationWindow: () => void; 8 | } 9 | 10 | const ChooseAppOrScreeenStep: React.FC = ( 11 | props: ChooseAppOrScreeenStepProps 12 | ) => { 13 | const { handleNextEntireScreen, handleNextApplicationWindow } = props; 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default ChooseAppOrScreeenStep; 36 | -------------------------------------------------------------------------------- /app/components/StepsOfStepper/ChooseAppOrScreenOverlay/PreviewGridList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ipcRenderer } from 'electron'; 3 | import { Row, Col } from 'react-flexbox-grid'; 4 | import SharingSourcePreviewCard from '../../SharingSourcePreviewCard'; 5 | import { IpcEvents } from '../../../main/IpcEvents.enum'; 6 | 7 | class PreviewGridListProps { 8 | viewSharingIds: string[] = []; 9 | 10 | isEntireScreen = true; 11 | 12 | handleNextEntireScreen = () => {}; 13 | 14 | handleNextApplicationWindow = () => {}; 15 | } 16 | 17 | export default function PreviewGridList(props: PreviewGridListProps) { 18 | const { 19 | viewSharingIds, 20 | isEntireScreen, 21 | handleNextEntireScreen, 22 | handleNextApplicationWindow, 23 | } = props; 24 | 25 | return ( 26 | 33 | {viewSharingIds.map((id) => { 34 | return ( 35 | 36 | { 40 | ipcRenderer.invoke(IpcEvents.SetDesktopCapturerSourceId, id); 41 | if (isEntireScreen) { 42 | handleNextEntireScreen(); 43 | } else { 44 | handleNextApplicationWindow(); 45 | } 46 | }} 47 | /> 48 | 49 | ); 50 | })} 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/components/StepsOfStepper/ChooseAppOrScreenOverlay/ViewSharingObject.d.ts: -------------------------------------------------------------------------------- 1 | type ViewSharingObject = { thumbnailUrl: string; name: string }; 2 | -------------------------------------------------------------------------------- /app/components/StepsOfStepper/ConfirmStep.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { ipcRenderer } from 'electron'; 3 | import { Text } from '@blueprintjs/core'; 4 | import { Row, Col } from 'react-flexbox-grid'; 5 | import { useTranslation } from 'react-i18next'; 6 | import SharingSourcePreviewCard from '../SharingSourcePreviewCard'; 7 | import DeviceInfoCallout from '../DeviceInfoCallout'; 8 | import { IpcEvents } from '../../main/IpcEvents.enum'; 9 | 10 | interface ConfirmStepProps { 11 | device: Device | null; 12 | } 13 | 14 | export default function ConfirmStep(props: ConfirmStepProps) { 15 | const { t } = useTranslation(); 16 | const { device } = props; 17 | const [ 18 | waitingForConnectionSharingSessionSourceId, 19 | setWaitingForConnectionSharingSessionSourceId, 20 | ] = useState(); 21 | 22 | useEffect(() => { 23 | ipcRenderer 24 | .invoke(IpcEvents.GetWaitingForConnectionSharingSessionSourceId) 25 | // eslint-disable-next-line promise/always-return 26 | .then((id) => { 27 | setWaitingForConnectionSharingSessionSourceId(id); 28 | }) 29 | .catch((e) => console.error(e)); 30 | }, []); 31 | 32 | return ( 33 |
34 | 35 | 36 | {t('Check if all is OK and click Confirm')} 37 | 38 | 39 | 40 | 41 | 48 | 49 | 50 | 53 | 54 | 55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /app/components/StepsOfStepper/ScanQRStep.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/ban-types */ 3 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 4 | import React from 'react'; 5 | import Enzyme from 'enzyme'; 6 | import Adapter from 'enzyme-adapter-react-16'; 7 | import ScanQRStep from './ScanQRStep'; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | jest.useFakeTimers(); 11 | 12 | const bp3QRCodeDialogRootSelector = '#bp3-qr-code-dialog-root'; 13 | const magnifyQRCodeButtonSelector = '#magnify-qr-code-button'; 14 | const qrCodeDialogInnerSelector = '#qr-code-dialog-inner'; 15 | 16 | describe('', () => { 17 | let wrapper = Enzyme.shallow(); 18 | 19 | beforeEach(() => { 20 | wrapper = Enzyme.shallow(); 21 | }); 22 | 23 | afterEach(() => { 24 | jest.clearAllMocks(); 25 | }); 26 | 27 | describe('when user clicks on magnify QR code button', () => { 28 | it('should set "QR Code Dialog Root" isOpen property to "true"', () => { 29 | // @ts-ignore 30 | wrapper.find(magnifyQRCodeButtonSelector).props().onClick(); 31 | // @ts-ignore 32 | expect(wrapper.find(bp3QRCodeDialogRootSelector).props().isOpen).toBe( 33 | true 34 | ); 35 | }); 36 | }); 37 | 38 | describe(`when magnified QR code dialog is opened, 39 | and when user closes magnified QR code dialog`, () => { 40 | it('should set "QR Code Dialog Root" isOpen property to "false"', () => { 41 | // @ts-ignore 42 | wrapper.find(magnifyQRCodeButtonSelector).props().onClick(); 43 | // @ts-ignore 44 | wrapper.find(bp3QRCodeDialogRootSelector).props().onClose(); 45 | // @ts-ignore 46 | expect(wrapper.find(bp3QRCodeDialogRootSelector).props().isOpen).toBe( 47 | false 48 | ); 49 | }); 50 | }); 51 | 52 | describe(`when magnified QR code dialog is opened, 53 | and when user clicks on qr code dialog inner`, () => { 54 | it('should set "QR Code Dialog Root" isOpen property to "false"', () => { 55 | // @ts-ignore 56 | wrapper.find(magnifyQRCodeButtonSelector).props().onClick(); 57 | // @ts-ignore 58 | wrapper.find(qrCodeDialogInnerSelector).props().onClick(); 59 | // @ts-ignore 60 | expect(wrapper.find(bp3QRCodeDialogRootSelector).props().isOpen).toBe( 61 | false 62 | ); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /app/components/ToggleThemeBtnGroup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useCallback } from 'react'; 2 | import { Button, Classes, ControlGroup } from '@blueprintjs/core'; 3 | import { ipcRenderer } from 'electron'; 4 | import { SettingsContext } from '../../containers/SettingsProvider'; 5 | import { IpcEvents } from '../../main/IpcEvents.enum'; 6 | 7 | export default function ToggleThemeBtnGroup() { 8 | const { isDarkTheme, setIsDarkThemeHook } = useContext(SettingsContext); 9 | 10 | const handleToggleDarkTheme = useCallback(() => { 11 | if (!isDarkTheme) { 12 | document.body.classList.toggle(Classes.DARK); 13 | setIsDarkThemeHook(true); 14 | } 15 | ipcRenderer.invoke(IpcEvents.NotifyAllSessionsWithAppThemeChanged); 16 | }, [isDarkTheme, setIsDarkThemeHook]); 17 | 18 | const handleToggleLightTheme = useCallback(() => { 19 | if (isDarkTheme) { 20 | document.body.classList.toggle(Classes.DARK); 21 | setIsDarkThemeHook(false); 22 | } 23 | ipcRenderer.invoke(IpcEvents.NotifyAllSessionsWithAppThemeChanged); 24 | }, [isDarkTheme, setIsDarkThemeHook]); 25 | 26 | return ( 27 | 28 |