├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .github ├── ISSUE_TEMPLATE │ └── bug-report.md └── workflows │ ├── ci.yml │ └── stale.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── LICENSE ├── README.md ├── SECURITY.md ├── app ├── .eslintrc.js ├── features │ ├── app │ │ ├── components │ │ │ ├── App.js │ │ │ └── index.js │ │ └── index.js │ ├── conference │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── components │ │ │ ├── Conference.js │ │ │ └── index.js │ │ ├── external_api.js │ │ ├── index.js │ │ └── styled │ │ │ ├── LoadingIndicator.js │ │ │ ├── Wrapper.js │ │ │ └── index.js │ ├── config │ │ └── index.js │ ├── navbar │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── components │ │ │ ├── HelpButton.js │ │ │ ├── Logo.js │ │ │ ├── Navbar.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── styled │ │ │ ├── DrawerContainer.js │ │ │ └── index.js │ ├── onboarding │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── components │ │ │ ├── Onboarding.js │ │ │ ├── OnboardingModal.js │ │ │ ├── OnboardingSpotlight.js │ │ │ └── index.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── middleware.js │ │ └── reducer.js │ ├── recent-list │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── components │ │ │ ├── RecentList.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── styled │ │ │ ├── ConferenceCard.js │ │ │ ├── ConferenceTitle.js │ │ │ ├── Label.js │ │ │ ├── RecentListContainer.js │ │ │ ├── RecentListWrapper.js │ │ │ ├── TruncatedText.js │ │ │ └── index.js │ │ └── types.js │ ├── redux │ │ ├── index.js │ │ ├── middleware.js │ │ ├── persistor.js │ │ ├── reducers.js │ │ └── store.js │ ├── router │ │ ├── history.js │ │ ├── index.js │ │ ├── middleware.js │ │ └── reducer.js │ ├── settings │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── components │ │ │ ├── ServerTimeoutField.js │ │ │ ├── ServerURLField.js │ │ │ ├── SettingToggle.js │ │ │ ├── SettingsButton.js │ │ │ ├── SettingsDrawer.js │ │ │ ├── ToggleWithLabel.js │ │ │ └── index.js │ │ ├── functions.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── styled │ │ │ ├── Form.js │ │ │ ├── Label.js │ │ │ ├── SettingsContainer.js │ │ │ ├── ToggleContainer.js │ │ │ ├── TogglesContainer.js │ │ │ └── index.js │ ├── utils │ │ ├── functions.js │ │ ├── index.js │ │ ├── openExternalLink.js │ │ └── parseURLParams.js │ └── welcome │ │ ├── components │ │ ├── Welcome.js │ │ └── index.js │ │ ├── index.js │ │ └── styled │ │ ├── Body.js │ │ ├── FieldWrapper.js │ │ ├── Form.js │ │ ├── Header.js │ │ ├── Label.js │ │ ├── Wrapper.js │ │ └── index.js ├── i18n │ ├── index.js │ └── lang │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── gl.json │ │ ├── hi.json │ │ ├── hr.json │ │ ├── hu.json │ │ ├── it.json │ │ ├── nl.json │ │ ├── pt-br.json │ │ ├── ru.json │ │ ├── sq.json │ │ ├── zh-CN.json │ │ └── zh-TW.json ├── images │ ├── logo.svg │ └── onboarding.png ├── index.html ├── index.js └── preload │ └── preload.js ├── main.js ├── package-lock.json ├── package.json ├── patches └── electron-reload+1.5.0.patch ├── resources ├── entitlements.mac.plist ├── entitlements.mas.inherit.plist ├── entitlements.mas.plist ├── icon.icns ├── icon.png └── icons │ └── 512x512.png ├── screenshot.png ├── screenshot_linux.png ├── webpack.main.js └── webpack.renderer.js /.eslintignore: -------------------------------------------------------------------------------- 1 | # The build artifacts of the jitsi-meet-electron project. 2 | Jitsi Meet* 3 | 4 | # ESLint will by default ignore its own configuration file. However, there does 5 | # not seem to be a reason why we will want to risk being inconsistent with our 6 | # remaining JavaScript source code. 7 | !.eslintrc.js 8 | 9 | build/ 10 | dist/ 11 | 12 | app/features/conference/external_api.js 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': [ 3 | 'eslint-config-jitsi' 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules/@atlaskit/.*/*.js.flow 3 | /node_modules/redux-persist/.*/*.js.flow 4 | /node_modules/resolve/test/resolver/malformed_package_json/package.json 5 | /build/.* 6 | /dist/.* 7 | 8 | [include] 9 | 10 | [libs] 11 | 12 | [lints] 13 | 14 | [options] 15 | 16 | [strict] 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Before posting, please make sure you check https://community.jitsi.org 4 | 5 | --- 6 | 7 | *This Issue tracker is only for reporting bugs and tracking code related issues.* 8 | 9 | Before posting, please make sure you check community.jitsi.org to see if the same or similar bugs have already been discussed. General questions, installation help, and feature requests can also be posted to community.jitsi.org. 10 | 11 | ## Description 12 | --- 13 | 14 | ## Current behavior 15 | --- 16 | 17 | ## Expected Behavior 18 | --- 19 | 20 | ## Possible Solution 21 | --- 22 | 23 | ## Steps to reproduce 24 | --- 25 | 26 | # Environment details 27 | --- 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # Trigger the workflow on push or pull request, 5 | # but only for the master branch 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build-linux: 15 | name: Linux 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: '20' 22 | - name: Build it 23 | env: 24 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | run: | 26 | npm ci 27 | npm run lint 28 | npm run dist 29 | - uses: actions/upload-artifact@v4 30 | with: 31 | name: linux-binaries 32 | path: | 33 | dist/jitsi-meet-amd64.deb 34 | dist/jitsi-meet-x86_64.AppImage 35 | dist/jitsi-meet-arm64.deb 36 | dist/jitsi-meet-arm64.AppImage 37 | dist/latest-linux.yml 38 | dist/latest-linux-arm64.yml 39 | build-mac: 40 | name: macOS 41 | runs-on: macos-14 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: actions/setup-node@v4 45 | with: 46 | node-version: '20' 47 | - name: Prepare for app signing and notarization 48 | env: 49 | MAC_CERT: ${{ secrets.mac_cert }} 50 | if: ${{ env.MAC_CERT }} 51 | run: | 52 | echo "CSC_LINK=${{ secrets.mac_cert }}" >> $GITHUB_ENV 53 | echo "CSC_KEY_PASSWORD=${{ secrets.mac_cert_password }}" >> $GITHUB_ENV 54 | echo "CSC_FOR_PULL_REQUEST=true" >> $GITHUB_ENV 55 | echo "APPLE_ID=${{ secrets.apple_id }}" >> $GITHUB_ENV 56 | echo "APPLE_APP_SPECIFIC_PASSWORD=${{ secrets.apple_id_password }}" >> $GITHUB_ENV 57 | echo "APPLE_TEAM_ID=${{ secrets.team_id }}" >> $GITHUB_ENV 58 | - name: Build it 59 | env: 60 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | run: | 62 | npm ci 63 | npm run dist 64 | - uses: actions/upload-artifact@v4 65 | with: 66 | name: mac-binaries 67 | path: | 68 | dist/jitsi-meet.dmg 69 | dist/jitsi-meet.zip 70 | dist/latest-mac.yml 71 | build-windows: 72 | name: Windows 73 | runs-on: windows-2022 74 | steps: 75 | - uses: actions/checkout@v4 76 | - uses: actions/setup-node@v4 77 | with: 78 | node-version: '20' 79 | - name: Build it 80 | env: 81 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 82 | run: | 83 | npm ci 84 | npm run dist 85 | - uses: actions/upload-artifact@v4 86 | with: 87 | name: windows-binaries 88 | path: | 89 | dist/jitsi-meet.exe 90 | dist/latest.yml 91 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v8 11 | with: 12 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 13 | stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 14 | stale-issue-label: 'stale' 15 | stale-pr-label: 'stale' 16 | exempt-issue-labels: 'confirmed' 17 | exempt-pr-labels: 'confirmed' 18 | days-before-issue-stale: 60 19 | days-before-pr-stale: 90 20 | days-before-issue-close: 10 21 | days-before-pr-close: 10 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .*.tmp 3 | .remote-sync.json 4 | .sync-config.cson 5 | .electron 6 | .electron-gyp 7 | .npmrc 8 | .idea 9 | .jshintignore 10 | .jshintrc 11 | 12 | .DS_Store 13 | 14 | # node.js 15 | # 16 | node_modules/ 17 | npm-debug.log 18 | 19 | build/ 20 | dist/ 21 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | ; FIXME: figure out why we need this... 3 | legacy-peer-deps=true 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jitsi Meet Electron 2 | 3 | Desktop application for [Jitsi Meet] built with [Electron]. 4 | 5 | ![](screenshot.png) 6 | 7 | ## Features 8 | 9 | - [End-to-End Encryption](https://jitsi.org/blog/e2ee/) support (BETA) 10 | - Works with any Jitsi Meet deployment 11 | - Built-in auto-updates 12 | - Screen sharing 13 | - ~Remote control~ (currently [disabled](https://github.com/jitsi/jitsi-meet-electron/issues/483) due to [security issues](https://github.com/jitsi/security-advisories/blob/master/advisories/JSA-2020-0001.md)) 14 | - Always-On-Top window 15 | - Support for deeplinks such as `jitsi-meet://myroom` (will open `myroom` on the configured Jitsi instance) or `jitsi-meet://jitsi.mycompany.com/myroom` (will open `myroom` on the Jitsi instance running on `jitsi.mycompany.com`) 16 | 17 | ## Installation 18 | 19 | Download our latest release and you're off to the races! 20 | 21 | | Windows | macOS | GNU/Linux (AppImage) | GNU/Linux (Deb) | 22 | | -- | -- | -- | -- | 23 | | [Download](https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet.exe) | [Download](https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet.dmg) | [x64_64](https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet-x86_64.AppImage) [arm64](https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet-arm64.AppImage) | [x86_64](https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet-amd64.deb) [arm64](https://github.com/jitsi/jitsi-meet-electron/releases/latest/download/jitsi-meet-arm64.deb) | 24 | 25 | ### Third-Party builds 26 | 27 | [Download On Flathub](https://flathub.org/apps/details/org.jitsi.jitsi-meet) 30 | 31 | ### Homebrew 32 | 33 | For *macOS* users, you can install the application using the following command: 34 | 35 | ``` 36 | brew install --cask jitsi-meet 37 | ``` 38 | 39 | ## Development 40 | 41 | If you want to hack on this project, here is how you do it. 42 | 43 |
Show building instructions 44 | 45 | #### Installing dependencies 46 | 47 | Install Node.js 20 first (or if you use [nvm](https://github.com/nvm-sh/nvm), switch to Node.js 20 by running `nvm use`). 48 | 49 |
Extra dependencies for Windows 50 | 51 | ```bash 52 | npm install --global --production windows-build-tools 53 | ``` 54 |
55 | 56 |
Extra dependencies for GNU/Linux 57 | 58 | X11, PNG, and zlib development packages are necessary. On Debian-like systems, they can be installed as follows: 59 | 60 | ```bash 61 | sudo apt install libx11-dev zlib1g-dev libpng-dev libxtst-dev 62 | ``` 63 |
64 | 65 | Install all required packages: 66 | 67 | ```bash 68 | npm install 69 | ``` 70 | 71 | #### Starting in development mode 72 | 73 | ```bash 74 | npm start 75 | ``` 76 | 77 | The debugger tools are available when running in dev mode, and can be activated with keyboard shortcuts as [defined here](https://github.com/sindresorhus/electron-debug#features). 78 | 79 | They can also be displayed automatically with the application `--show-dev-tools` command line flag, or with the `SHOW_DEV_TOOLS` environment variable as shown: 80 | 81 | ```bash 82 | SHOW_DEV_TOOLS=true npm start 83 | ``` 84 | 85 | #### Building the production distribution 86 | 87 | ```bash 88 | npm run dist 89 | ``` 90 | 91 | #### Working with `jitsi-meet-electron-sdk` 92 | 93 | [`jitsi-meet-electron-sdk`] is a helper package which implements many features 94 | such as remote control and the always-on-top window. If new features are to be 95 | added or tested, running with a local version of these utils is very handy. 96 | 97 | By default, the @jitsi/electron-sdk is build from `npm`. The default dependency path in `package.json` is: 98 | 99 | ```json 100 | "@jitsi/electron-sdk": "^3.0.0" 101 | ``` 102 | 103 | To work with a local copy, you must change the path to: 104 | 105 | ```json 106 | "@jitsi/electron-sdk": "file:///Users/name/jitsi-meet-electron-sdk-copy", 107 | ``` 108 | 109 | To build the project, you must force it to take the sources, as `npm update` will 110 | not do it. 111 | 112 | ```bash 113 | npm install @jitsi/electron-sdk --force 114 | ``` 115 | 116 | NOTE: Also check the [`jitsi-meet-electron-sdk` `README`] to see how to configure 117 | your environment. 118 | 119 | #### Publishing 120 | 121 | 1. Create release branch: `git checkout -b release-1-2-3`, replacing `1-2-3` with the desired release version 122 | 2. Increment the version: `npm version patch`, replacing `patch` with `minor` or `major` as required 123 | 3. Push release branch to github: `git push -u origin release-1-2-3` 124 | 4. Create PR: `gh pr create` 125 | 5. Once PR is reviewed and ready to merge, create draft Github release: `gh release create v1.2.3 --draft --title 1.2.3`, replacing `v1.2.3` and `1.2.3` with the desired release version 126 | 6. Merge PR 127 | 7. Github action will build binaries and attach to the draft release 128 | 8. Test binaries from draft release 129 | 9. If all tests are fine, publish draft release 130 | 131 |
132 | 133 | ## Known issues 134 | 135 | ### Windows 136 | 137 | A warning that the app is unsigned will show up upon first install. This is expected. 138 | 139 | ### macOS 140 | 141 | None 142 | 143 | ### GNU/Linux 144 | 145 | * If you can't execute the file directly after downloading it, try running `chmod u+x ./jitsi-meet-x86_64.AppImage` 146 | 147 | * On Ubuntu 22.04 and later, the AppImage will fail with a FUSE error (as the AppImage uses `libfuse2`, while 22.04 comes with `libfuse3` by default): 148 | 149 | ``` 150 | dlopen(): error loading libfuse.so.2 151 | ``` 152 | 153 | To fix this, install `libfuse2` as follows: 154 | 155 | ``` 156 | sudo apt install libfuse2 157 | ``` 158 | 159 | * On Ubuntu 24.04 and later, the AppImage will fail with a sandboxing error (`The SUID sandbox helper binary was found, but is not configured correctly...`) 160 | This is due to an AppArmor conflict that restricts unprivileged user namespaces ([jitsi/jitsi-meet-electron#965](https://github.com/jitsi/jitsi-meet-electron/issues/965), 161 | [Ubuntu blog post](https://ubuntu.com/blog/ubuntu-23-10-restricted-unprivileged-user-namespaces)). 162 | 163 | To work around this, disable the use of the sandbox with `--no-sandbox`: 164 | 165 | ``` 166 | ./jitsi-meet-x86_64.AppImage --no-sandbox 167 | ``` 168 | 169 | * If you experience a blank page after a Jitsi server upgrades, try removing the local cache files: 170 | 171 | ``` 172 | rm -rf ~/.config/Jitsi\ Meet/ 173 | ``` 174 | 175 | ## Translations 176 | 177 | The JSON files contain all the strings inside the application, and can be translated [here](/app/i18n/lang). 178 | 179 | New translations require the addition of a line in [index.js](/app/i18n/index.js). 180 | 181 | `Localize desktop file on linux` requires the addition of a line in [package.json](/package.json). 182 | Please search for `Comment[hu]` as an example to help add your translation of the English string `Jitsi Meet Desktop App` for your language. 183 | 184 | ## License 185 | 186 | Apache License 2.0. See the [LICENSE] file. 187 | 188 | ## Community 189 | 190 | Jitsi is built by a large community of developers. If you want to participate, 191 | please join the [community forum]. 192 | 193 | [Jitsi Meet]: https://github.com/jitsi/jitsi-meet 194 | [Electron]: https://electronjs.org/ 195 | [latest release]: https://github.com/jitsi/jitsi-meet-electron/releases/latest 196 | [`jitsi-meet-electron-sdk`]: https://github.com/jitsi/jitsi-meet-electron-sdk 197 | [`jitsi-meet-electron-sdk` `README`]: https://github.com/jitsi/jitsi-meet-electron-sdk/blob/master/README.md 198 | [community forum]: https://community.jitsi.org/ 199 | [LICENSE]: LICENSE 200 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Reporting security issues 4 | 5 | We take security very seriously and develop all Jitsi projects to be secure and safe. 6 | 7 | If you find (or simply suspect) a security issue in any of the Jitsi projects, please send us an email to security@jitsi.org. 8 | 9 | **We encourage responsible disclosure for the sake of our users, so please reach out before posting in a public space.** 10 | -------------------------------------------------------------------------------- /app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': [ 3 | './../.eslintrc.js', 4 | 'eslint-config-jitsi/jsdoc', 5 | 'eslint-config-jitsi/react' 6 | ] 7 | }; 8 | -------------------------------------------------------------------------------- /app/features/app/components/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { AtlasKitThemeProvider } from '@atlaskit/theme'; 4 | 5 | import React, { Component } from 'react'; 6 | import { Route, Switch } from 'react-router'; 7 | import { connect } from 'react-redux'; 8 | import { ConnectedRouter as Router, push } from 'react-router-redux'; 9 | 10 | import { Conference } from '../../conference'; 11 | import config from '../../config'; 12 | import { history } from '../../router'; 13 | import { createConferenceObjectFromURL } from '../../utils'; 14 | import { Welcome } from '../../welcome'; 15 | 16 | /** 17 | * Main component encapsulating the entire application. 18 | */ 19 | class App extends Component<*> { 20 | /** 21 | * Initializes a new {@code App} instance. 22 | * 23 | * @inheritdoc 24 | */ 25 | constructor(props) { 26 | super(props); 27 | 28 | document.title = config.appName; 29 | 30 | this._listenOnProtocolMessages 31 | = this._listenOnProtocolMessages.bind(this); 32 | } 33 | 34 | /** 35 | * Implements React's {@link Component#componentDidMount()}. 36 | * 37 | * @returns {void} 38 | */ 39 | componentDidMount() { 40 | // start listening on this events 41 | window.jitsiNodeAPI.ipc.on('protocol-data-msg', this._listenOnProtocolMessages); 42 | 43 | // send notification to main process 44 | window.jitsiNodeAPI.ipc.send('renderer-ready'); 45 | } 46 | 47 | /** 48 | * Implements React's {@link Component#componentWillUnmount()}. 49 | * 50 | * @returns {void} 51 | */ 52 | componentWillUnmount() { 53 | // remove listening for this events 54 | window.jitsiNodeAPI.ipc.removeListener( 55 | 'protocol-data-msg', 56 | this._listenOnProtocolMessages 57 | ); 58 | } 59 | 60 | _listenOnProtocolMessages: (*) => void; 61 | 62 | /** 63 | * Handler when main proccess contact us. 64 | * 65 | * @param {Object} event - Message event. 66 | * @param {string} inputURL - String with room name. 67 | * 68 | * @returns {void} 69 | */ 70 | _listenOnProtocolMessages(event, inputURL: string) { 71 | // Remove trailing slash if one exists. 72 | if (inputURL.slice(-1) === '/') { 73 | inputURL = inputURL.slice(0, -1); // eslint-disable-line no-param-reassign 74 | } 75 | 76 | const conference = createConferenceObjectFromURL(inputURL); 77 | 78 | // Don't navigate if conference couldn't be created 79 | if (!conference) { 80 | return; 81 | } 82 | 83 | // change route when we are notified 84 | this.props.dispatch(push('/conference', conference)); 85 | } 86 | 87 | /** 88 | * Implements React's {@link Component#render()}. 89 | * 90 | * @inheritdoc 91 | * @returns {ReactElement} 92 | */ 93 | render() { 94 | return ( 95 | 96 | 97 | 98 | 102 | 105 | 106 | 107 | 108 | ); 109 | } 110 | } 111 | 112 | export default connect()(App); 113 | -------------------------------------------------------------------------------- /app/features/app/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as App } from './App'; 2 | -------------------------------------------------------------------------------- /app/features/app/index.js: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | -------------------------------------------------------------------------------- /app/features/conference/actionTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The type of (redux) action that is dispatched when conference is joined. 3 | * 4 | * @type { 5 | * type: CONFERENCE_JOINED, 6 | * conference: Object 7 | * } 8 | */ 9 | export const CONFERENCE_JOINED = Symbol('CONFERENCE_JOINED'); 10 | 11 | /** 12 | * The type of (redux) action that is dispatched when conference ends. 13 | * 14 | * @type { 15 | * type: CONFERENCE_ENDED, 16 | * conference: Object 17 | * } 18 | */export const CONFERENCE_ENDED = Symbol('CONFERENCE_ENDED'); 19 | -------------------------------------------------------------------------------- /app/features/conference/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { CONFERENCE_JOINED, CONFERENCE_ENDED } from './actionTypes'; 4 | 5 | /** 6 | * Notifies that conference is joined. 7 | * 8 | * @param {Object} conference - Conference Details. 9 | * @returns {{ 10 | * type: CONFERENCE_JOINED, 11 | * conference: Object 12 | * }} 13 | */ 14 | export function conferenceJoined(conference: Object) { 15 | return { 16 | type: CONFERENCE_JOINED, 17 | conference 18 | }; 19 | } 20 | 21 | /** 22 | * Notifies that conference is joined. 23 | * 24 | * @param {Object} conference - Conference Details. 25 | * @returns {{ 26 | * type: CONFERENCE_ENDED, 27 | * conference: Object 28 | * }} 29 | */ 30 | export function conferenceEnded(conference: Object) { 31 | return { 32 | type: CONFERENCE_ENDED, 33 | conference 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /app/features/conference/components/Conference.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Spinner from '@atlaskit/spinner'; 4 | 5 | import React, { Component } from 'react'; 6 | import type { Dispatch } from 'redux'; 7 | import { connect } from 'react-redux'; 8 | import { push } from 'react-router-redux'; 9 | 10 | import i18n from '../../../i18n'; 11 | import config from '../../config'; 12 | import { getSetting } from '../../settings'; 13 | import { parseURLParams } from '../../utils/parseURLParams'; 14 | 15 | import { conferenceEnded, conferenceJoined } from '../actions'; 16 | import JitsiMeetExternalAPI from '../external_api'; 17 | import { LoadingIndicator, Wrapper } from '../styled'; 18 | 19 | // For enabling remote control, please change ENABLE_REMOTE_CONTROL flag in 20 | // main.js to true as well 21 | const ENABLE_REMOTE_CONTROL = false; 22 | 23 | type Props = { 24 | 25 | /** 26 | * Redux dispatch. 27 | */ 28 | dispatch: Dispatch<*>; 29 | 30 | /** 31 | * React Router location object. 32 | */ 33 | location: Object; 34 | 35 | /** 36 | * AlwaysOnTop Window Enabled. 37 | */ 38 | _alwaysOnTopWindowEnabled: boolean; 39 | 40 | /** 41 | * Disable automatic gain control. 42 | */ 43 | _disableAGC: boolean; 44 | 45 | /** 46 | * Default Jitsi Server URL. 47 | */ 48 | _serverURL: string; 49 | 50 | /** 51 | * Default Jitsi Server Timeout. 52 | */ 53 | _serverTimeout: number; 54 | }; 55 | 56 | type State = { 57 | 58 | /** 59 | * If the conference is loading or not. 60 | */ 61 | isLoading: boolean; 62 | }; 63 | 64 | /** 65 | * Conference component. 66 | */ 67 | class Conference extends Component { 68 | /** 69 | * External API object. 70 | */ 71 | _api: Object; 72 | 73 | /** 74 | * Conference Object. 75 | */ 76 | _conference: Object; 77 | 78 | /** 79 | * Whether the iframe was loaded or not. 80 | */ 81 | _iframeLoaded: boolean; 82 | 83 | /** 84 | * Timer to cancel the joining if it takes too long. 85 | */ 86 | _loadTimer: ?TimeoutID; 87 | 88 | /** 89 | * Reference to the element of this component. 90 | */ 91 | _ref: Object; 92 | 93 | /** 94 | * Initializes a new {@code Conference} instance. 95 | * 96 | * @inheritdoc 97 | */ 98 | constructor() { 99 | super(); 100 | 101 | this.state = { 102 | isLoading: true 103 | }; 104 | 105 | this._ref = React.createRef(); 106 | 107 | this._onIframeLoad = this._onIframeLoad.bind(this); 108 | this._onVideoConferenceEnded = this._onVideoConferenceEnded.bind(this); 109 | } 110 | 111 | /** 112 | * Attach the script to this component. 113 | * 114 | * @returns {void} 115 | */ 116 | componentDidMount() { 117 | const room = this.props.location.state.room; 118 | const serverTimeout = this.props._serverTimeout || config.defaultServerTimeout; 119 | const serverURL = this.props.location.state.serverURL 120 | || this.props._serverURL 121 | || config.defaultServerURL; 122 | 123 | this._conference = { 124 | room, 125 | serverURL 126 | }; 127 | 128 | this._loadConference(); 129 | 130 | // Set a timer for a timeout duration, if we haven't loaded the iframe by then, 131 | // give up. 132 | this._loadTimer = setTimeout(() => { 133 | this._navigateToHome( 134 | 135 | // $FlowFixMe 136 | { 137 | error: 'Loading error', 138 | type: 'error' 139 | }, 140 | room, 141 | serverURL); 142 | }, serverTimeout * 1000); 143 | } 144 | 145 | /** 146 | * Remove conference on unmounting. 147 | * 148 | * @returns {void} 149 | */ 150 | componentWillUnmount() { 151 | if (this._loadTimer) { 152 | clearTimeout(this._loadTimer); 153 | } 154 | if (this._api) { 155 | this._api.dispose(); 156 | } 157 | } 158 | 159 | /** 160 | * Handle joining another another meeing while in one. 161 | * 162 | * @param {Object} prevProps - The previous props. 163 | * @returns {void} 164 | */ 165 | componentDidUpdate(prevProps) { 166 | if (prevProps.location.key !== this.props.location.key) { 167 | 168 | // Simulate a re-mount so the new meeting is joined. 169 | this._iframeLoaded = false; 170 | this.componentWillUnmount(); 171 | this.componentDidMount(); 172 | } 173 | } 174 | 175 | /** 176 | * Implements React's {@link Component#render()}. 177 | * 178 | * @returns {ReactElement} 179 | */ 180 | render() { 181 | return ( 182 | 183 | { this._maybeRenderLoadingIndicator() } 184 | 185 | ); 186 | } 187 | 188 | /** 189 | * Load the conference by creating the iframe element in this component 190 | * and attaching utils from jitsi-meet-electron-utils. 191 | * 192 | * @returns {void} 193 | */ 194 | _loadConference() { 195 | const appProtocolSurplus = `${config.appProtocolPrefix}://`; 196 | 197 | // replace the custom url with https, otherwise new URL() raises 'Invalid URL'. 198 | if (this._conference.serverURL.startsWith(appProtocolSurplus)) { 199 | this._conference.serverURL = this._conference.serverURL.replace(appProtocolSurplus, 'https://'); 200 | } 201 | const url = new URL(this._conference.room, this._conference.serverURL); 202 | const roomName = url.pathname.split('/').pop(); 203 | const host = this._conference.serverURL.replace(/https?:\/\//, ''); 204 | const searchParameters = Object.fromEntries(url.searchParams); 205 | const hashParameters = parseURLParams(url); 206 | 207 | const locale = { lng: i18n.language }; 208 | const urlParameters = { 209 | ...searchParameters, 210 | ...locale 211 | }; 212 | 213 | 214 | const configOverwrite = { 215 | enableCalendarIntegration: false, 216 | disableAGC: this.props._disableAGC, 217 | prejoinConfig: { 218 | enabled: true 219 | } 220 | }; 221 | 222 | const interfaceConfigOverwrite = { 223 | SHOW_CHROME_EXTENSION_BANNER: false 224 | }; 225 | let jwt; 226 | 227 | Object.entries(hashParameters).forEach(([ key, value ]) => { 228 | if (key.startsWith('config.')) { 229 | const configKey = key.substring('config.'.length); 230 | 231 | configOverwrite[configKey] = value; 232 | } else if (key === 'jwt') { 233 | jwt = value; 234 | } 235 | }); 236 | 237 | const options = { 238 | configOverwrite, 239 | interfaceConfigOverwrite, 240 | jwt, 241 | parentNode: this._ref.current, 242 | roomName, 243 | sandbox: 'allow-scripts allow-same-origin allow-popups allow-forms allow-downloads' 244 | }; 245 | 246 | this._api = new JitsiMeetExternalAPI(host, { 247 | ...options, 248 | ...urlParameters 249 | }); 250 | 251 | // This event is fired really early, at the same time as 'ready', but has been 252 | // around for longer. 253 | // TODO: remove after a while. -saghul 254 | this._api.on('browserSupport', this._onIframeLoad); 255 | 256 | this._api.on('suspendDetected', this._onVideoConferenceEnded); 257 | this._api.on('readyToClose', this._onVideoConferenceEnded); 258 | this._api.on('videoConferenceJoined', 259 | () => { 260 | this.props.dispatch(conferenceJoined(this._conference)); 261 | } 262 | ); 263 | 264 | // Setup Jitsi Meet Electron SDK on this renderer. 265 | window.jitsiNodeAPI.setupRenderer(this._api, { 266 | enableRemoteControl: ENABLE_REMOTE_CONTROL, 267 | enableAlwaysOnTopWindow: this.props._alwaysOnTopWindowEnabled 268 | }); 269 | } 270 | 271 | /** 272 | * It renders a loading indicator, if appropriate. 273 | * 274 | * @returns {?ReactElement} 275 | */ 276 | _maybeRenderLoadingIndicator() { 277 | if (this.state.isLoading) { 278 | return ( 279 | 280 | 281 | 282 | ); 283 | } 284 | } 285 | 286 | /** 287 | * Navigates to home screen (Welcome). 288 | * 289 | * @param {Event} event - Event by which the function is called. 290 | * @param {string} room - Room name. 291 | * @param {string} serverURL - Server URL. 292 | * @returns {void} 293 | */ 294 | _navigateToHome(event: Event, room: ?string, serverURL: ?string) { 295 | this.props.dispatch(push('/', { 296 | error: event.type === 'error', 297 | room, 298 | serverURL 299 | })); 300 | } 301 | 302 | _onVideoConferenceEnded: (*) => void; 303 | 304 | /** 305 | * Dispatches conference ended and navigates to home screen. 306 | * 307 | * @param {Event} event - Event by which the function is called. 308 | * @returns {void} 309 | * @private 310 | */ 311 | _onVideoConferenceEnded(event: Event) { 312 | this.props.dispatch(conferenceEnded(this._conference)); 313 | this._navigateToHome(event); 314 | } 315 | 316 | _onIframeLoad: (*) => void; 317 | 318 | /** 319 | * Sets state of loading to false when iframe has completely loaded. 320 | * 321 | * @returns {void} 322 | */ 323 | _onIframeLoad() { 324 | if (this._iframeLoaded) { 325 | // Skip spurious event after meeting close. 326 | return; 327 | } 328 | 329 | console.log('IFrame loaded'); 330 | 331 | this._iframeLoaded = true; 332 | 333 | if (this._loadTimer) { 334 | clearTimeout(this._loadTimer); 335 | this._loadTimer = null; 336 | } 337 | 338 | this.setState({ 339 | isLoading: false 340 | }); 341 | } 342 | } 343 | 344 | /** 345 | * Maps (parts of) the redux state to the React props. 346 | * 347 | * @param {Object} state - The redux state. 348 | * @returns {Props} 349 | */ 350 | function _mapStateToProps(state: Object) { 351 | return { 352 | _alwaysOnTopWindowEnabled: getSetting(state, 'alwaysOnTopWindowEnabled', true), 353 | _disableAGC: state.settings.disableAGC, 354 | _serverURL: state.settings.serverURL, 355 | _serverTimeout: state.settings.serverTimeout 356 | }; 357 | } 358 | 359 | export default connect(_mapStateToProps)(Conference); 360 | -------------------------------------------------------------------------------- /app/features/conference/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Conference } from './Conference'; 2 | -------------------------------------------------------------------------------- /app/features/conference/index.js: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './actionTypes'; 3 | export * from './components'; 4 | export * from './styled'; 5 | -------------------------------------------------------------------------------- /app/features/conference/styled/LoadingIndicator.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import styled from 'styled-components'; 4 | 5 | export default styled.div` 6 | align-items: center; 7 | display: flex; 8 | height: 100%; 9 | justify-content: center; 10 | `; 11 | -------------------------------------------------------------------------------- /app/features/conference/styled/Wrapper.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import styled from 'styled-components'; 4 | 5 | export default styled.div` 6 | height: 100vh; 7 | `; 8 | -------------------------------------------------------------------------------- /app/features/conference/styled/index.js: -------------------------------------------------------------------------------- 1 | export { default as LoadingIndicator } from './LoadingIndicator'; 2 | export { default as Wrapper } from './Wrapper'; 3 | -------------------------------------------------------------------------------- /app/features/config/index.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | /** 4 | * The URL with extra information about the app / service. 5 | */ 6 | aboutURL: 'https://jitsi.org/what-is-jitsi/', 7 | 8 | /** 9 | * The URL to the source code repository. 10 | */ 11 | sourceURL: 'https://github.com/jitsi/jitsi-meet-electron', 12 | 13 | /** 14 | * Application name. 15 | */ 16 | appName: 'Jitsi Meet', 17 | 18 | /** 19 | * The prefix for application protocol. 20 | * You will also need to replace this in package.json. 21 | */ 22 | appProtocolPrefix: 'jitsi-meet', 23 | 24 | /** 25 | * The default server URL of Jitsi Meet Deployment that will be used. 26 | */ 27 | defaultServerURL: 'https://meet.jit.si', 28 | 29 | /** 30 | * The default server Timeout in seconds. 31 | */ 32 | defaultServerTimeout: 30, 33 | 34 | /** 35 | * URL to send feedback. 36 | */ 37 | feedbackURL: 'https://github.com/jitsi/jitsi-meet-electron/issues', 38 | 39 | /** 40 | * The URL of Privacy Policy Page. 41 | */ 42 | privacyPolicyURL: 'https://jitsi.org/meet/privacy', 43 | 44 | /** 45 | * The URL of Terms and Conditions Page. 46 | */ 47 | termsAndConditionsURL: 'https://jitsi.org/meet/terms' 48 | }; 49 | -------------------------------------------------------------------------------- /app/features/navbar/actionTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The type of (redux) action that opens specified Drawer. 3 | * 4 | * @type { 5 | * type: OPEN_DRAWER, 6 | * drawerComponent: React.ComponentType<*> 7 | * } 8 | */ 9 | export const OPEN_DRAWER = Symbol('OPEN_DRAWER'); 10 | 11 | /** 12 | * The type of (redux) action that closes all Drawer. 13 | * 14 | * @type { 15 | * type: CLOSE_DRAWER 16 | * } 17 | */ 18 | export const CLOSE_DRAWER = Symbol('CLOSE_DRAWER'); 19 | -------------------------------------------------------------------------------- /app/features/navbar/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ComponentType } from 'react'; 4 | 5 | import { CLOSE_DRAWER, OPEN_DRAWER } from './actionTypes'; 6 | 7 | /** 8 | * Closes the drawers. 9 | * 10 | * @returns {{ 11 | * type: CLOSE_DRAWER, 12 | * }} 13 | */ 14 | export function closeDrawer() { 15 | return { 16 | type: CLOSE_DRAWER 17 | }; 18 | } 19 | 20 | /** 21 | * Opens the specified drawer. 22 | * 23 | * @param {string} drawerComponent - Component of the drawer. 24 | * @returns {{ 25 | * type: OPEN_DRAWER, 26 | * drawerComponent: ComponentType<*> 27 | * }} 28 | */ 29 | export function openDrawer(drawerComponent: ComponentType<*>) { 30 | return { 31 | type: OPEN_DRAWER, 32 | drawerComponent 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /app/features/navbar/components/HelpButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Droplist, { Item, Group } from '@atlaskit/droplist'; 4 | import HelpIcon from '@atlaskit/icon/glyph/question-circle'; 5 | 6 | import React, { Component } from 'react'; 7 | import { withTranslation } from 'react-i18next'; 8 | 9 | import config from '../../config'; 10 | import { openExternalLink } from '../../utils'; 11 | import { version } from '../../../../package.json'; 12 | 13 | type State = { 14 | 15 | /** 16 | * Whether the droplist is open or not. 17 | */ 18 | droplistOpen: boolean 19 | }; 20 | 21 | /** 22 | * Help button for Navigation Bar. 23 | */ 24 | class HelpButton extends Component<*, State> { 25 | /** 26 | * Initializes a new {@code HelpButton} instance. 27 | * 28 | * @inheritdoc 29 | */ 30 | constructor() { 31 | super(); 32 | 33 | this.state = { 34 | droplistOpen: false 35 | }; 36 | 37 | this._onAboutClick = openExternalLink.bind(undefined, config.aboutURL); 38 | this._onSourceClick = openExternalLink.bind(undefined, config.sourceURL); 39 | this._onIconClick = this._onIconClick.bind(this); 40 | this._onOpenChange = this._onOpenChange.bind(this); 41 | this._onPrivacyClick 42 | = openExternalLink.bind(undefined, config.privacyPolicyURL); 43 | this._onTermsClick 44 | = openExternalLink.bind(undefined, config.termsAndConditionsURL); 45 | this._onSendFeedbackClick 46 | = openExternalLink.bind(undefined, config.feedbackURL); 47 | } 48 | 49 | _onAboutClick: (*) => void; 50 | 51 | _onSourceClick: (*) => void; 52 | 53 | _onIconClick: (*) => void; 54 | 55 | /** 56 | * Toggles the droplist. 57 | * 58 | * @returns {void} 59 | */ 60 | _onIconClick() { 61 | this.setState({ 62 | droplistOpen: !this.state.droplistOpen 63 | }); 64 | } 65 | 66 | _onOpenChange: (*) => void; 67 | 68 | /** 69 | * Closes droplist when clicked outside. 70 | * 71 | * @returns {void} 72 | */ 73 | _onOpenChange() { 74 | this.setState({ 75 | droplistOpen: false 76 | }); 77 | } 78 | 79 | _onPrivacyClick: (*) => void; 80 | 81 | _onTermsClick: (*) => void; 82 | 83 | _onSendFeedbackClick: (*) => void; 84 | 85 | /** 86 | * Render function of component. 87 | * 88 | * @returns {ReactElement} 89 | */ 90 | render() { 91 | const { t } = this.props; 92 | 93 | return ( 94 | }> 100 | 101 | 102 | { t('termsLink') } 103 | 104 | 105 | { t('privacyLink') } 106 | 107 | 108 | { t('sendFeedbackLink') } 109 | 110 | 111 | { t('aboutLink') } 112 | 113 | 114 | { t('sourceLink') } 115 | 116 | 117 | { t('versionLabel', { version }) } 118 | 119 | 120 | 121 | ); 122 | } 123 | } 124 | 125 | export default withTranslation()(HelpButton); 126 | -------------------------------------------------------------------------------- /app/features/navbar/components/Logo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Icon from '@atlaskit/icon'; 4 | 5 | import React, { Component } from 'react'; 6 | 7 | import LogoSVG from '../../../images/logo.svg'; 8 | 9 | /** 10 | * Logo component. 11 | */ 12 | export default class Logo extends Component<*> { 13 | 14 | /** 15 | * Render function of component. 16 | * 17 | * @returns {ReactElement} 18 | */ 19 | render() { 20 | return ( 21 | 24 | ); 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /app/features/navbar/components/Navbar.js: -------------------------------------------------------------------------------- 1 | 2 | // @flow 3 | 4 | import Navigation, { AkGlobalItem } from '@atlaskit/navigation'; 5 | 6 | import React, { Component } from 'react'; 7 | import { connect } from 'react-redux'; 8 | 9 | import { SettingsButton, SettingsDrawer } from '../../settings'; 10 | 11 | import HelpButton from './HelpButton'; 12 | import Logo from './Logo'; 13 | 14 | type Props = { 15 | 16 | /** 17 | * Whether Settings Drawer is open or not. 18 | */ 19 | _isSettingsDrawerOpen: boolean; 20 | }; 21 | 22 | /** 23 | * Navigation Bar component. 24 | */ 25 | class Navbar extends Component { 26 | /** 27 | * Get the array of Primary actions of Global Navigation. 28 | * 29 | * @returns {ReactElement[]} 30 | */ 31 | _getPrimaryActions() { 32 | return [ 33 | 34 | 35 | 36 | ]; 37 | } 38 | 39 | /** 40 | * Get the array of Secondary actions of Global Navigation. 41 | * 42 | * @returns {ReactElement[]} 43 | */ 44 | _getSecondaryActions() { 45 | return [ 46 | 47 | 48 | 49 | ]; 50 | } 51 | 52 | /** 53 | * Render function of component. 54 | * 55 | * @returns {ReactElement} 56 | */ 57 | render() { 58 | return ( 59 | 64 | ] } 65 | globalPrimaryActions = { this._getPrimaryActions() } 66 | globalPrimaryIcon = { } 67 | globalSecondaryActions = { this._getSecondaryActions() } 68 | isOpen = { false } 69 | isResizeable = { false } /> 70 | ); 71 | } 72 | } 73 | 74 | /** 75 | * Maps (parts of) the redux state to the React props. 76 | * 77 | * @param {Object} state - The redux state. 78 | * @returns {{ 79 | * _isSettingsDrawerOpen: boolean 80 | * }} 81 | */ 82 | function _mapStateToProps(state: Object) { 83 | return { 84 | _isSettingsDrawerOpen: state.navbar.openDrawer === SettingsDrawer 85 | }; 86 | } 87 | 88 | 89 | export default connect(_mapStateToProps)(Navbar); 90 | -------------------------------------------------------------------------------- /app/features/navbar/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Logo } from './Logo'; 2 | export { default as Navbar } from './Navbar'; 3 | -------------------------------------------------------------------------------- /app/features/navbar/index.js: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './actionTypes'; 3 | export * from './components'; 4 | export * from './styled'; 5 | 6 | export { default as reducer } from './reducer'; 7 | -------------------------------------------------------------------------------- /app/features/navbar/reducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ComponentType } from 'react'; 4 | 5 | import { CLOSE_DRAWER, OPEN_DRAWER } from './actionTypes'; 6 | 7 | type State = { 8 | openDrawer: typeof undefined | ComponentType<*> 9 | }; 10 | 11 | const DEFAULT_STATE = { 12 | openDrawer: undefined 13 | }; 14 | 15 | /** 16 | * Reduces redux actions for features/settings. 17 | * 18 | * @param {State} state - Current reduced redux state. 19 | * @param {Object} action - Action which was dispatched. 20 | * @returns {State} - Updated reduced redux state. 21 | */ 22 | export default (state: State = DEFAULT_STATE, action: Object) => { 23 | switch (action.type) { 24 | case CLOSE_DRAWER: 25 | return { 26 | ...state, 27 | openDrawer: undefined 28 | }; 29 | 30 | case OPEN_DRAWER: 31 | return { 32 | ...state, 33 | openDrawer: action.drawerComponent 34 | }; 35 | 36 | default: 37 | return state; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /app/features/navbar/styled/DrawerContainer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import styled from 'styled-components'; 4 | 5 | export default styled.div` 6 | margin-right: 68px; 7 | padding: 0 0.8em; 8 | `; 9 | -------------------------------------------------------------------------------- /app/features/navbar/styled/index.js: -------------------------------------------------------------------------------- 1 | export { default as DrawerContainer } from './DrawerContainer'; 2 | -------------------------------------------------------------------------------- /app/features/onboarding/actionTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The type of (redux) action that continues the onboarding by processing 3 | * the next step. 4 | * 5 | * @type { 6 | * type: CONTINUE_ONBOARDING 7 | * } 8 | */ 9 | export const CONTINUE_ONBOARDING = Symbol('CONTINUE_ONBOARDING'); 10 | 11 | /** 12 | * The type of (redux) action that sets active onboarding. 13 | * 14 | * @type { 15 | * type: SET_ACTIVE_ONBOARDING, 16 | * name: string, 17 | * section: string 18 | * } 19 | */ 20 | export const SET_ACTIVE_ONBOARDING = Symbol('SET_ACTIVE_ONBOARDING'); 21 | 22 | /** 23 | * The type of (redux) action that starts Onboarding. 24 | * 25 | * @type { 26 | * type: START_ONBOARDING, 27 | * section: string 28 | * } 29 | */ 30 | export const START_ONBOARDING = Symbol('START_ONBOARDING'); 31 | 32 | /** 33 | * The type of (redux) action that skips all onboarding. 34 | * 35 | * @type { 36 | * type: SKIP_ONBOARDING 37 | * } 38 | */ 39 | export const SKIP_ONBOARDING = Symbol('SKIP_ONBOARDING'); 40 | -------------------------------------------------------------------------------- /app/features/onboarding/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | CONTINUE_ONBOARDING, 5 | SET_ACTIVE_ONBOARDING, 6 | SKIP_ONBOARDING, 7 | START_ONBOARDING 8 | } from './actionTypes'; 9 | 10 | /** 11 | * Continues the onboarding procedure by activating the next step of the current 12 | * section. 13 | * 14 | * @returns {{ 15 | * type: CONTINUE_ONBOARDING 16 | * }} 17 | */ 18 | export function continueOnboarding() { 19 | return { 20 | type: CONTINUE_ONBOARDING 21 | }; 22 | } 23 | 24 | /** 25 | * Set active onboarding. 26 | * 27 | * @param {string} name - Name of onboarding component. 28 | * @param {string} section - Onboarding section. 29 | * @returns {{ 30 | * type: SET_ACTIVE_ONBOARDING, 31 | * name: string, 32 | * section: string 33 | * }} 34 | */ 35 | export function setActiveOnboarding(name: string, section: string) { 36 | return { 37 | type: SET_ACTIVE_ONBOARDING, 38 | name, 39 | section 40 | }; 41 | } 42 | 43 | /** 44 | * Skips onboarding. 45 | * 46 | * @returns {{ 47 | * type: SKIP_ONBOARDING 48 | * }} 49 | */ 50 | export function skipOnboarding() { 51 | return { 52 | type: SKIP_ONBOARDING 53 | }; 54 | } 55 | 56 | /** 57 | * Start onboarding. 58 | * 59 | * @param {string} section - Onboarding section. 60 | * @returns {{ 61 | * type: START_ONBOARDING, 62 | * section: string 63 | * }} 64 | */ 65 | export function startOnboarding(section: string) { 66 | return { 67 | type: START_ONBOARDING, 68 | section 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /app/features/onboarding/components/Onboarding.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component } from 'react'; 4 | import { connect } from 'react-redux'; 5 | import type { Dispatch } from 'redux'; 6 | 7 | import { onboardingComponents, onboardingSteps } from '../../onboarding'; 8 | 9 | type Props = { 10 | 11 | /** 12 | * Redux dispatch. 13 | */ 14 | dispatch: Dispatch<*>; 15 | 16 | /** 17 | * Onboarding Section. 18 | */ 19 | section: string; 20 | 21 | /** 22 | * Active Onboarding. 23 | */ 24 | _activeOnboarding: string; 25 | }; 26 | 27 | /** 28 | * Onboarding Component Entry Point. 29 | */ 30 | class Onboarding extends Component { 31 | /** 32 | * Render function of component. 33 | * 34 | * @returns {ReactElement} 35 | */ 36 | render() { 37 | const { section, _activeOnboarding } = this.props; 38 | const steps = onboardingSteps[section]; 39 | 40 | if (_activeOnboarding && steps.includes(_activeOnboarding)) { 41 | const { type: ActiveOnboarding, ...props } = onboardingComponents[_activeOnboarding]; 42 | 43 | return ; 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | 50 | /** 51 | * Maps (parts of) the redux state to the React props. 52 | * 53 | * @param {Object} state - The redux state. 54 | * @returns {{ 55 | * _activeOnboarding: string 56 | * }} 57 | */ 58 | function _mapStateToProps(state: Object) { 59 | return { 60 | _activeOnboarding: state.onboarding.activeOnboarding 61 | }; 62 | } 63 | 64 | export default connect(_mapStateToProps)(Onboarding); 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/features/onboarding/components/OnboardingModal.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Modal } from '@atlaskit/onboarding'; 4 | 5 | import React, { Component } from 'react'; 6 | import { withTranslation } from 'react-i18next'; 7 | import { connect } from 'react-redux'; 8 | import type { Dispatch } from 'redux'; 9 | import { compose } from 'redux'; 10 | 11 | import OnboardingModalImage from '../../../images/onboarding.png'; 12 | 13 | import config from '../../config'; 14 | 15 | import { skipOnboarding, continueOnboarding } from '../actions'; 16 | 17 | type Props = { 18 | 19 | /** 20 | * Redux dispatch. 21 | */ 22 | dispatch: Dispatch<*>; 23 | 24 | /** 25 | * I18next translation function. 26 | */ 27 | t: Function; 28 | }; 29 | 30 | /** 31 | * Onboarding Modal Component. 32 | */ 33 | class OnboardingModal extends Component { 34 | /** 35 | * Initializes a new {@code OnboardingModal} instance. 36 | * 37 | * @inheritdoc 38 | */ 39 | constructor(props: Props) { 40 | super(props); 41 | 42 | // Bind event handlers. 43 | this._skip = this._skip.bind(this); 44 | this._next = this._next.bind(this); 45 | } 46 | 47 | /** 48 | * Render function of component. 49 | * 50 | * @returns {ReactElement} 51 | */ 52 | render() { 53 | const { t } = this.props; 54 | 55 | return ( 56 | 69 |

{ t('onboarding.letUsShowYouAround') }

70 |
71 | ); 72 | } 73 | 74 | _next: (*) => void; 75 | 76 | /** 77 | * Close the spotlight component. 78 | * 79 | * @returns {void} 80 | */ 81 | _next() { 82 | this.props.dispatch(continueOnboarding()); 83 | } 84 | 85 | _skip: (*) => void; 86 | 87 | /** 88 | * Skips all the onboardings. 89 | * 90 | * @returns {void} 91 | */ 92 | _skip() { 93 | this.props.dispatch(skipOnboarding()); 94 | } 95 | 96 | } 97 | 98 | export default compose(connect(), withTranslation())(OnboardingModal); 99 | -------------------------------------------------------------------------------- /app/features/onboarding/components/OnboardingSpotlight.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Spotlight } from '@atlaskit/onboarding'; 4 | 5 | import React from 'react'; 6 | import { useTranslation } from 'react-i18next'; 7 | import { connect } from 'react-redux'; 8 | import type { Dispatch } from 'redux'; 9 | 10 | import { continueOnboarding } from '../actions'; 11 | 12 | type Props = { 13 | 14 | /** 15 | * Redux dispatch. 16 | */ 17 | dispatch: Dispatch<*>; 18 | 19 | /** 20 | * Spotlight dialog placement. 21 | */ 22 | dialogPlacement: String; 23 | 24 | /** 25 | * Callback when "next" clicked. 26 | */ 27 | onNext: Function; 28 | 29 | /** 30 | * I18next translation function. 31 | */ 32 | t: Function; 33 | 34 | /** 35 | * Spotlight target. 36 | */ 37 | target: String; 38 | 39 | /** 40 | * Spotlight text. 41 | */ 42 | text: String; 43 | 44 | }; 45 | 46 | const OnboardingSpotlight = (props: Props) => { 47 | const { t } = useTranslation(); 48 | 49 | return ( 50 | { 54 | props.dispatch(continueOnboarding()); 55 | props.onNext && props.onNext(props); 56 | }, 57 | text: t('onboarding.next') 58 | } 59 | ] } 60 | dialogPlacement = { props.dialogPlacement } 61 | target = { props.target } > 62 | { t(props.text) } 63 | 64 | ); 65 | }; 66 | 67 | 68 | export default connect()(OnboardingSpotlight); 69 | -------------------------------------------------------------------------------- /app/features/onboarding/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as OnboardingSpotlight } from './OnboardingSpotlight'; 2 | export { default as Onboarding } from './Onboarding'; 3 | export { default as OnboardingModal } from './OnboardingModal'; 4 | -------------------------------------------------------------------------------- /app/features/onboarding/constants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { OnboardingModal, OnboardingSpotlight } from './components'; 3 | import { openDrawer, closeDrawer } from '../navbar'; 4 | import { SettingsDrawer } from '../settings'; 5 | 6 | export const onboardingSteps = { 7 | 'welcome-page': [ 8 | 'onboarding-modal', 9 | 'conference-url', 10 | 'settings-drawer-button' 11 | ], 12 | 'settings-drawer': [ 13 | 'server-setting', 14 | 'server-timeout', 15 | 'always-on-top-window' 16 | ] 17 | }; 18 | 19 | export const onboardingComponents = { 20 | 'onboarding-modal': { type: OnboardingModal }, 21 | 'conference-url': { 22 | type: OnboardingSpotlight, 23 | dialogPlacement: 'bottom center', 24 | target: 'conference-url', 25 | text: 'onboarding.conferenceUrl' 26 | }, 27 | 'settings-drawer-button': { 28 | type: OnboardingSpotlight, 29 | dialogPlacement: 'top right', 30 | target: 'settings-drawer-button', 31 | text: 'onboarding.settingsDrawerButton', 32 | onNext: (props: OnboardingSpotlight.props) => props.dispatch(openDrawer(SettingsDrawer)) 33 | }, 34 | 'server-setting': { 35 | type: OnboardingSpotlight, 36 | dialogPlacement: 'top right', 37 | target: 'server-setting', 38 | text: 'onboarding.serverSetting' 39 | }, 40 | 'server-timeout': { 41 | type: OnboardingSpotlight, 42 | dialogPlacement: 'top right', 43 | target: 'server-timeout', 44 | text: 'onboarding.serverTimeout' 45 | }, 46 | 'always-on-top-window': { 47 | type: OnboardingSpotlight, 48 | dialogPlacement: 'top right', 49 | target: 'always-on-top-window', 50 | text: 'onboarding.alwaysOnTop', 51 | onNext: (props: OnboardingSpotlight.props) => setTimeout(() => { 52 | props.dispatch(closeDrawer()); 53 | }, 300) 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /app/features/onboarding/index.js: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './actionTypes'; 3 | export * from './components'; 4 | export * from './constants'; 5 | 6 | export { default as middleware } from './middleware'; 7 | export { default as reducer } from './reducer'; 8 | -------------------------------------------------------------------------------- /app/features/onboarding/middleware.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { setActiveOnboarding } from './actions'; 4 | import { CONTINUE_ONBOARDING, START_ONBOARDING } from './actionTypes'; 5 | import { onboardingSteps } from './constants'; 6 | 7 | export default (store: Object) => (next: Function) => (action: Object) => { 8 | const result = next(action); 9 | const state = store.getState(); 10 | 11 | switch (action.type) { 12 | case CONTINUE_ONBOARDING: { 13 | const section = state.onboarding.activeOnboardingSection; 14 | 15 | const nextStep = onboardingSteps[section].find( 16 | step => !state.onboarding.onboardingShown.includes(step) 17 | ); 18 | 19 | store.dispatch(setActiveOnboarding(nextStep, nextStep && section)); 20 | break; 21 | } 22 | 23 | case START_ONBOARDING: { 24 | const { section } = action; 25 | const nextStep = onboardingSteps[section].find( 26 | step => !state.onboarding.onboardingShown.includes(step) 27 | ); 28 | 29 | if (nextStep) { 30 | store.dispatch(setActiveOnboarding(nextStep, section)); 31 | } 32 | break; 33 | } 34 | } 35 | 36 | return result; 37 | }; 38 | -------------------------------------------------------------------------------- /app/features/onboarding/reducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | CONTINUE_ONBOARDING, 5 | SET_ACTIVE_ONBOARDING, 6 | SKIP_ONBOARDING 7 | } from './actionTypes'; 8 | import { onboardingSteps } from './constants'; 9 | 10 | type State = { 11 | activeOnboarding: ?string; 12 | activeOnboardingSection: ?string; 13 | onboardingShown: Array; 14 | }; 15 | 16 | const DEFAULT_STATE = { 17 | activeOnboarding: undefined, 18 | activeOnboardingSection: undefined, 19 | onboardingShown: [] 20 | }; 21 | 22 | /** 23 | * Reduces redux actions for features/onboarding. 24 | * 25 | * @param {State} state - Current reduced redux state. 26 | * @param {Object} action - Action which was dispatched. 27 | * @returns {State} - Updated reduced redux state. 28 | */ 29 | export default (state: State = DEFAULT_STATE, action: Object) => { 30 | switch (action.type) { 31 | case CONTINUE_ONBOARDING: 32 | return { 33 | ...state, 34 | activeOnboarding: undefined, 35 | onboardingShown: 36 | 37 | // $FlowFixMe 38 | state.onboardingShown.concat(state.activeOnboarding) 39 | }; 40 | 41 | case SET_ACTIVE_ONBOARDING: 42 | return { 43 | ...state, 44 | activeOnboarding: action.name, 45 | activeOnboardingSection: action.section 46 | }; 47 | 48 | case SKIP_ONBOARDING: { 49 | // $FlowFixMe 50 | const allSteps = [].concat(...Object.values(onboardingSteps)); 51 | 52 | return { 53 | ...state, 54 | activeOnboarding: undefined, 55 | activeOnboardingSection: undefined, 56 | onboardingShown: 57 | 58 | // $FlowFixMe 59 | state.onboardingShown.concat(allSteps) 60 | }; 61 | } 62 | 63 | default: 64 | return state; 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /app/features/recent-list/actionTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The type of (redux) action that is dispatched when a conference is removed from the recents list. 3 | * 4 | * @type { 5 | * type: CONFERENCE_REMOVED, 6 | * conference: Object 7 | * } 8 | */ 9 | export const CONFERENCE_REMOVED = Symbol('CONFERENCE_REMOVED'); 10 | 11 | -------------------------------------------------------------------------------- /app/features/recent-list/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { CONFERENCE_REMOVED } from './actionTypes'; 4 | 5 | /** 6 | * Notifies that conference is removed from recents list. 7 | * 8 | * @param {Object} conference - Conference Details. 9 | * @returns {{ 10 | * type: CONFERENCE_REMOVED, 11 | * conference: Object 12 | * }} 13 | */ 14 | export function conferenceRemoved(conference: Object) { 15 | return { 16 | type: CONFERENCE_REMOVED, 17 | conference 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /app/features/recent-list/components/RecentList.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import moment from 'moment'; 4 | import React, { Component } from 'react'; 5 | import { withTranslation } from 'react-i18next'; 6 | import { connect } from 'react-redux'; 7 | import type { Dispatch } from 'redux'; 8 | import { compose } from 'redux'; 9 | import { push } from 'react-router-redux'; 10 | 11 | import { conferenceRemoved } from '../actions'; 12 | import { 13 | ConferenceCard, 14 | ConferenceTitle, 15 | Label, 16 | RecentListContainer, 17 | RecentListWrapper, 18 | TruncatedText 19 | } from '../styled'; 20 | import type { RecentListItem } from '../types'; 21 | import Button from '@atlaskit/button'; 22 | import CrossIcon from '@atlaskit/icon/glyph/cross'; 23 | 24 | type Props = { 25 | 26 | /** 27 | * Redux dispatch. 28 | */ 29 | dispatch: Dispatch<*>; 30 | 31 | /** 32 | * Array of recent conferences. 33 | */ 34 | _recentList: Array; 35 | 36 | /** 37 | * I18next translation function. 38 | */ 39 | t: Function; 40 | }; 41 | 42 | /** 43 | * Recent List Component. 44 | */ 45 | class RecentList extends Component { 46 | /** 47 | * Render function of component. 48 | * 49 | * @returns {ReactElement} 50 | */ 51 | render() { 52 | const { t } = this.props; 53 | 54 | if (this.props._recentList.length === 0) { 55 | return null; 56 | } 57 | 58 | return ( 59 | 60 | 61 | 62 | { 63 | this.props._recentList.map( 64 | conference => this._renderRecentListEntry(conference) 65 | ) 66 | } 67 | 68 | 69 | ); 70 | } 71 | 72 | /** 73 | * Creates a handler for navigatint to a conference. 74 | * 75 | * @param {RecentListItem} conference - Conference Details. 76 | * @returns {void} 77 | */ 78 | _onNavigateToConference(conference: RecentListItem) { 79 | return () => this.props.dispatch(push('/conference', conference)); 80 | } 81 | 82 | /** 83 | * Creates a handler for removing a conference from the recents list. 84 | * 85 | * @param {RecentListItem} conference - Conference Details. 86 | * @returns {void} 87 | */ 88 | _onRemoveConference(conference: RecentListItem) { 89 | return e => { 90 | this.props.dispatch(conferenceRemoved(conference)); 91 | e.stopPropagation(); 92 | }; 93 | } 94 | 95 | 96 | /** 97 | * Renders the conference card. 98 | * 99 | * @param {RecentListItem} conference - Conference Details. 100 | * @returns {ReactElement} 101 | */ 102 | _renderRecentListEntry(conference: RecentListItem) { 103 | return ( 104 | 107 | 108 | { conference.room } 109 | 110 | 111 | { this._renderServerURL(conference.serverURL) } 112 | 113 | 114 | { this._renderStartTime(conference) } 115 | 116 | 117 | { this._renderDuration(conference) } 118 | 119 | 284 | 285 | 286 | 287 | 288 | ); 289 | } 290 | 291 | _updateRoomname: () => void; 292 | 293 | /** 294 | * Triggers the generation of a new room name and initiates an animation of 295 | * its changing. 296 | * 297 | * @protected 298 | * @returns {void} 299 | */ 300 | _updateRoomname() { 301 | const generatedRoomname = generateRoomWithoutSeparator(); 302 | const roomPlaceholder = ''; 303 | const updateTimeoutId = setTimeout(this._updateRoomname, 10000); 304 | 305 | this._clearTimeouts(); 306 | this.setState( 307 | { 308 | generatedRoomname, 309 | roomPlaceholder, 310 | updateTimeoutId 311 | }, 312 | () => this._animateRoomnameChanging(generatedRoomname)); 313 | } 314 | } 315 | 316 | export default compose(connect(), withTranslation())(Welcome); 317 | -------------------------------------------------------------------------------- /app/features/welcome/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Welcome } from './Welcome'; 2 | -------------------------------------------------------------------------------- /app/features/welcome/index.js: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './styled'; 3 | -------------------------------------------------------------------------------- /app/features/welcome/styled/Body.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import styled from 'styled-components'; 4 | 5 | export default styled.div` 6 | margin: 0 12.5%; 7 | overflow: scroll; 8 | 9 | ::-webkit-scrollbar { 10 | display: none; 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /app/features/welcome/styled/FieldWrapper.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import styled from 'styled-components'; 4 | 5 | export default styled.div` 6 | align-items: center; 7 | display: flex; 8 | justify-content: space-between; 9 | magin: auto; 10 | `; 11 | -------------------------------------------------------------------------------- /app/features/welcome/styled/Form.js: -------------------------------------------------------------------------------- 1 | // Flow 2 | 3 | import styled from 'styled-components'; 4 | 5 | export default styled.form` 6 | width: 26em; 7 | margin: 0 1.2em; 8 | `; 9 | -------------------------------------------------------------------------------- /app/features/welcome/styled/Header.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import styled from 'styled-components'; 4 | 5 | export default styled.div` 6 | align-items: center; 7 | display: flex; 8 | margin: 0 auto; 9 | padding: 8em; 10 | `; 11 | -------------------------------------------------------------------------------- /app/features/welcome/styled/Label.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import styled from 'styled-components'; 4 | 5 | export default styled.span` 6 | color: white; 7 | font-weight: bold; 8 | `; 9 | -------------------------------------------------------------------------------- /app/features/welcome/styled/Wrapper.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import styled from 'styled-components'; 4 | 5 | export default styled.div` 6 | background: #1D69D4; 7 | display: flex; 8 | flex-direction: column; 9 | height: 100vh; 10 | `; 11 | -------------------------------------------------------------------------------- /app/features/welcome/styled/index.js: -------------------------------------------------------------------------------- 1 | export { default as Body } from './Body'; 2 | export { default as FieldWrapper } from './FieldWrapper'; 3 | export { default as Form } from './Form'; 4 | export { default as Header } from './Header'; 5 | export { default as Label } from './Label'; 6 | export { default as Wrapper } from './Wrapper'; 7 | -------------------------------------------------------------------------------- /app/i18n/index.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import moment from 'moment'; 4 | 5 | const languages = { 6 | de: { translation: require('./lang/de.json') }, 7 | en: { translation: require('./lang/en.json') }, 8 | es: { translation: require('./lang/es.json') }, 9 | fr: { translation: require('./lang/fr.json') }, 10 | gl: { translation: require('./lang/gl.json') }, 11 | hi: { translation: require('./lang/hi.json') }, 12 | hr: { translation: require('./lang/hr.json') }, 13 | hu: { translation: require('./lang/hu.json') }, 14 | it: { translation: require('./lang/it.json') }, 15 | nl: { translation: require('./lang/nl.json') }, 16 | pt: { translation: require('./lang/pt-br.json') }, 17 | ru: { translation: require('./lang/ru.json') }, 18 | sq: { translation: require('./lang/sq.json') }, 19 | 'zh-CN': { translation: require('./lang/zh-CN.json') }, 20 | 'zh-TW': { translation: require('./lang/zh-TW.json') } 21 | }; 22 | 23 | const detectedLocale = navigator.language; 24 | 25 | i18n 26 | .use(initReactI18next) 27 | .init({ 28 | resources: languages, 29 | lng: detectedLocale, 30 | fallbackLng: 'en', 31 | interpolation: { 32 | escapeValue: false // not needed for react as it escapes by default 33 | } 34 | }); 35 | 36 | moment.locale(detectedLocale); 37 | 38 | export default i18n; 39 | -------------------------------------------------------------------------------- /app/i18n/lang/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Bitte einen Konferenznamen oder eine Jitsi-Adresse eingeben", 3 | "go": "LOS", 4 | "help": "Hilfe", 5 | "termsLink": "Nutzungsbedingungen", 6 | "privacyLink": "Datenschutzbedingungen", 7 | "recentListLabel": "oder einen zuletzt genutzen Konferenzraum betreten", 8 | "sendFeedbackLink": "Eine Rückmeldung senden", 9 | "aboutLink": "F&A", 10 | "sourceLink": "Quelltext", 11 | "versionLabel": "Version: {{version}}", 12 | "onboarding": { 13 | "startTour": "Tour starten", 14 | "skip": "Überspringen", 15 | "welcome": "Willkommen in {{appName}}", 16 | "letUsShowYouAround": "Wir zeigen wie alles funktioniert!", 17 | "next": "Weiter", 18 | "conferenceUrl": "Bitte den Namen (oder die vollständige Adresse) des Raumes eingeben, dem beigetreten werden soll. Es kann ein Name ausgedacht werden, diesen bitte anderen mitteilen, damit sie denselben Namen eingeben.", 19 | "settingsDrawerButton": "Hier klicken, um zu den Einstellungen zu gelangen.", 20 | "serverSetting": "Das ist der Server, auf dem die Konferenzen stattfinden werden. Es kann ein eigener verwendet werden, muss aber nicht!", 21 | "serverTimeout": "Zeitüberschreitung für den Beitritt zu einer Konferenz. Wenn nicht rechtzeitig beigetreten wurde, wird die Konferenz abgebrochen.", 22 | "alwaysOnTop": "Hier kann eingestellt werden, ob das Fenster »Immer im Vordergrund« aktiviert wird. Dieses wird angezeigt, wenn das Hauptfenster den Fokus verliert. Das wird bei allen Konferenzen angewendet." 23 | }, 24 | "settings": { 25 | "back": "Zurück", 26 | "alwaysOnTopWindow": "Immer im Vordergrund", 27 | "invalidServer": "Falsche Server-Adresse", 28 | "invalidServerTimeout": "Ungültiger Wert für die Server-Wartezeit", 29 | "serverUrl": "Server-Adresse", 30 | "serverTimeout": "Server-Wartezeit (in Sekunden)", 31 | "disableAGC": "Automatische Mikrofonlautstärkeregelung deaktivieren" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/i18n/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Enter a room name for your conference or a Jitsi URL", 3 | "go": "GO", 4 | "help": "Help", 5 | "termsLink": "Terms", 6 | "privacyLink": "Privacy", 7 | "recentListLabel": "or rejoin one of your recent conference rooms", 8 | "sendFeedbackLink": "Send Feedback", 9 | "aboutLink": "About", 10 | "sourceLink": "Source Code", 11 | "versionLabel": "Version: {{version}}", 12 | "onboarding": { 13 | "startTour": "Start Tour", 14 | "skip": "Skip", 15 | "welcome": "Welcome to {{appName}}", 16 | "letUsShowYouAround": "Let us show you around!", 17 | "next": "Next", 18 | "conferenceUrl": "Enter the name (or full URL) of the room you want to join. You may make a name up, just let others know so they enter the same name.", 19 | "settingsDrawerButton": "Click here to open the settings drawer.", 20 | "serverSetting": "This will be the server where your conferences will take place. You can use your own, but you don't need to!", 21 | "serverTimeout": "Timeout to join a meeting, if the meeting hasn't been joined before the timeout hits, it's cancelled.", 22 | "alwaysOnTop": "You can toggle whether you want to enable the \"always-on-top\" window, which is displayed when the main window loses focus. This will be applied to all conferences." 23 | }, 24 | "settings": { 25 | "back": "Back", 26 | "alwaysOnTopWindow": "Always on Top Window", 27 | "invalidServer": "Invalid Server URL", 28 | "invalidServerTimeout": "Invalid value for Server Timeout", 29 | "serverUrl": "Server URL", 30 | "serverTimeout": "Server Timeout (in seconds)", 31 | "disableAGC": "Disable automatic gain control" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/i18n/lang/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Ingrese un nombre para su conferencia o un URL de Jitsi", 3 | "go": "IR", 4 | "help": "Ayuda", 5 | "termsLink": "Condiciones de Uso", 6 | "privacyLink": "Privacidad", 7 | "sendFeedbackLink": "Enviar comentarios", 8 | "aboutLink": "Acerca", 9 | "sourceLink": "Código fuente", 10 | "versionLabel": "Version: {{version}}", 11 | "onboarding": { 12 | "startTour": "Iniciar Tour", 13 | "skip": "Saltar", 14 | "welcome": "Bienvenido a {{appName}}", 15 | "letUsShowYouAround": "¡Permítanos mostrárselo!", 16 | "next": "Siguiente", 17 | "conferenceUrl": "Ingrese el nombre (o URL completo) de la sala a que se quiera unir. Puede inventar el nombre, solo informe a otros para que ellos usen el mismo nombre.", 18 | "settingsDrawerButton": "Click aquí para abrir la opción de configuración.", 19 | "serverSetting": "Este será el servidor donde sus conferencias tomarán lugar. Puede usar el suyo, ¡pero no necesita!", 20 | "serverTimeout": "Tiempo de desconexión, si nadie se une a la reunión antes del tiempo de desconexión termina, esta se cancela.", 21 | "alwaysOnTop": "Puede seleccionar si desea activar la ventana \"siempre-encima\", la que se muestra cuando la ventana principal pierde enfoque. Esto será aplicado a todas las conferencias." 22 | }, 23 | "settings": { 24 | "back": "Atrás", 25 | "alwaysOnTopWindow": "Ventana siempre encima", 26 | "invalidServer": "URL del servidor inválida", 27 | "serverUrl": "URL del servidor", 28 | "serverTimeout": "Tiempo de desconexión del servidor (en segundos)" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/i18n/lang/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Entrez un nom pour votre conférence ou une URL Jitsi", 3 | "go": "Rejoindre", 4 | "help": "Aide", 5 | "termsLink": "Conditions d’utilisation", 6 | "privacyLink": "Politique de confidentialité", 7 | "sendFeedbackLink": "Envoyer des commentaires", 8 | "aboutLink": "À propos", 9 | "sourceLink": "Code source", 10 | "versionLabel": "Version : {{version}}", 11 | "onboarding": { 12 | "startTour": "Démarrez le tutoriel", 13 | "skip": "Passer", 14 | "welcome": "Bienvenue sur {{appName}}", 15 | "letUsShowYouAround": "Laissez-nous vous montrer!", 16 | "next": "Suivant", 17 | "conferenceUrl": "Entrez le nom (ou l’URL complète) de la conférence que vous souhaitez rejoindre. Vous pouvez choisir ce que vous voulez, dites simplement à vos contacts d’utiliser le même nom.", 18 | "settingsDrawerButton": "Cliquez ici pour ouvrir le panneau de configuration.", 19 | "serverSetting": "Ceci est le serveur utilisé pour vos conférences. Vous pouvez utiliser le vôtre, mais vous n’êtes pas obligés !", 20 | "serverTimeout": "Délai pour rejoindre la conférence. Si la conférence ne s’est pas ouverte avant ce délai, elle sera abandonnée.", 21 | "alwaysOnTop": "Vous pouvez activer la fenêtre \"Toujours au Dessus\", qui sera affichée quand la fenêtre principale n’est plus au premier plan. Ceci sera appliqué à toutes les conférences." 22 | }, 23 | "settings": { 24 | "back": "Retour", 25 | "alwaysOnTopWindow": "Fenêtre Toujours au Dessus", 26 | "invalidServer": "URL invalide", 27 | "serverUrl": "URL du serveur", 28 | "serverTimeout": "Délai de connexion au server (en secondes)" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/i18n/lang/gl.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Introduza un nome para a conferencia ou un URL de Jitsi", 3 | "go": "IR", 4 | "help": "Axuda", 5 | "termsLink": "Termos", 6 | "privacyLink": "Privacidade", 7 | "sendFeedbackLink": "Enviar unha opinión", 8 | "aboutLink": "Sobre", 9 | "sourceLink": "Código fonte", 10 | "versionLabel": "Versión: {{version}}", 11 | "onboarding": { 12 | "startTour": "Comezar a visita guiada", 13 | "skip": "Omitir", 14 | "welcome": "Reciba a benvida a {{appName}}", 15 | "letUsShowYouAround": "Permita que lle mostremos o que hai!", 16 | "next": "Seguinte", 17 | "conferenceUrl": "Introduza o nome (ou URL completo) da sala á que desexa sumarse. Pode dar de alta un nome e permitir que a xente coa que se reúne o saiba para que introduzan o mesmo nome.", 18 | "settingsDrawerButton": "Prema aquí para abrir a gabeta de configuración.", 19 | "serverSetting": "Este será o servidor no que se leven a cabo as súas conferencias. Pode usar unha propia, mais non ten por que o facer!", 20 | "serverTimeout": "Tempo de agarda para se sumar a unha reunión; se non se sumou antes de que remate, cancélase.", 21 | "alwaysOnTop": "Pode alternar entre activar e desactivar a xanela «sempre por riba», que se mostra cando a xanela principal perde o foco. Isto é aplicábel a todas as conferencias." 22 | }, 23 | "settings": { 24 | "back": "Atrás", 25 | "alwaysOnTopWindow": "Xanela sempre por riba", 26 | "invalidServer": "O URL do servidor é incorrecto", 27 | "invalidServerTimeout": "O tempo de agarda do servidor é incorrecto", 28 | "serverUrl": "URL do servidor", 29 | "serverTimeout": "Tempo de agarda do servidor (en segundos)" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/i18n/lang/hi.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "अपने सम्मेलन के लिए रुम का नाम या जित्सी लिंक दर्ज करें।", 3 | "go": "शुरु करें", 4 | "help": "सहायता", 5 | "termsLink": "शरतें", 6 | "privacyLink": "गोपनीयता", 7 | "recentListLabel": "या अपने हाल के सम्मेलनों में से किसी एक में पुनः शामिल हों।", 8 | "sendFeedbackLink": "प्रतिक्रिया भेजें", 9 | "aboutLink": "हमारे बारे में", 10 | "sourceLink": "सोर्स कोड", 11 | "versionLabel": "संस्करण: {{version}}", 12 | "onboarding": { 13 | "startTour": "एैप का दौरा करें।", 14 | "skip": "स्किप करें", 15 | "welcome": "{{appName}} में आपका स्वागत है!", 16 | "letUsShowYouAround": "हमें आपको एैप दिखाने दें।", 17 | "next": "आगे", 18 | "conferenceUrl": "अपने रुम का नााम (या लिंक) प्रदान करें। आप कोई भी नाम रख सकते हैं, बस रूम में बाकी लोगों को यह नाम बता दें ताकि वह हमें यहि नाम प्रदान करें।", 19 | "settingsDrawerButton": "यहा कलि्क कर सेटिंगस खोलें।", 20 | "serverSetting": "इस सर्वर पर आपकी कॉन्फ्रेंस होंगी। आप स्वयं का सर्वर भी उपयोग कर सकते हैं, लेकिन आपको इसकी आवश्यकता नहीं है!", 21 | "serverTimeout": "मीटींग टाइमआउट हो गयी हैं, यदि टाइमआउट से पहले मीटिंग में जुड़ा नहीं जा सकता तो मीटिंग रद्द कर दि जाति है।", 22 | "alwaysOnTop": "आप टॉगल कर सकते हैं कि क्या आप \"ऑलवेज़-ऑन-टॉप\" विंडो को इनेबल करना चाहते हैं, जो तब प्रदर्शित होती है जब मुख्य विंडो फोकस खो देती है। यह सभी कॉन्फ्रेंसो में लागू होंगी।" 23 | }, 24 | "settings": { 25 | "back": "पिछे", 26 | "alwaysOnTopWindow": "ऑलवेज़-ऑन-टॉप", 27 | "invalidServer": "अमान्य सर्वर URL", 28 | "invalidServerTimeout": "यह संख्या सर्वर टाइमआउट के लिए अमान्य है!", 29 | "serverUrl": "सर्वर URL", 30 | "serverTimeout": "सर्वर टाइमआउट (सकेंड में)", 31 | "disableAGC": "स्वचालित नियंत्रण बंद करें" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/i18n/lang/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Upiši ime sobe za tvoju konferenciju ili Jitsi URL", 3 | "go": "KRENI", 4 | "help": "Pomoć", 5 | "termsLink": "Uvjeti", 6 | "privacyLink": "Privatnost", 7 | "recentListLabel": "ili se ponovo pridruži jednoj od tvojih nedavnih konferencijskih soba", 8 | "sendFeedbackLink": "Pošalji povratnu informaciju", 9 | "aboutLink": "Informacije o", 10 | "sourceLink": "Izvroni kod", 11 | "versionLabel": "Verzija: {{version}}", 12 | "onboarding": { 13 | "startTour": "Započni obilazak", 14 | "skip": "Preskoči", 15 | "welcome": "Pozdrav u {{appName}}", 16 | "letUsShowYouAround": "Upoznaj se s radom aplikacije!", 17 | "next": "Dalje", 18 | "conferenceUrl": "Upiši ime (ili potpuni URL) sobe kojoj se želiš pridružiti. Izmisli si ime, ali obavijesti druge kako bi ga upisali.", 19 | "settingsDrawerButton": "Pritisni ovdje za otvaranje postavki.", 20 | "serverSetting": "Ovo će biti poslužitelj na kojem će se održavati tvoje konferencije. Možeš koristiti vlastiti poslužitelj, ali ne moraš!", 21 | "serverTimeout": "Vremensko ograničenje za pridruživanje sastanku. Ako se sastanku nitko ne pridruži prije tog vremena, sastanak se otkazuje.", 22 | "alwaysOnTop": "Ovdje se može uključiti postavljanje prozora „Uvijek ispred ostalih”, koji se prikazuje kada glavni prozor nije aktivan prozor. Ovo će se primijeniti na sve konferencije." 23 | }, 24 | "settings": { 25 | "back": "Natrag", 26 | "alwaysOnTopWindow": "Uvijek ispred ostalih", 27 | "invalidServer": "Neispravna URL-adresa poslužitelja", 28 | "invalidServerTimeout": "Neispravna vrijednost za vremensko ograničenje poslužitelja", 29 | "serverUrl": "URL-adresa poslužitelja", 30 | "serverTimeout": "Vremensko ograničenje poslužitelja (u sekundama)", 31 | "disableAGC": "Isključi kontrolu za automasko pojačanje zvuka" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/i18n/lang/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Adja meg a konferencia nevét vagy a Jitsi URL-címét", 3 | "go": "Indítás", 4 | "help": "Súgó", 5 | "termsLink": "Feltételek", 6 | "privacyLink": "Adatvédelem", 7 | "recentListLabel": "vagy csatlakozzon újra az egyik legutóbbi konferenciateremhez", 8 | "sendFeedbackLink": "Visszajelzés", 9 | "aboutLink": "Névjegy", 10 | "sourceLink": "Forráskód", 11 | "versionLabel": "Verzió: {{version}}", 12 | "onboarding": { 13 | "startTour": "Bemutató indítása", 14 | "skip": "Kihagyás", 15 | "welcome": "Isten hozott: {{appName}}!", 16 | "letUsShowYouAround": "Hadd mutassuk meg Neked!", 17 | "next": "Következő", 18 | "conferenceUrl": "Írja be a csatlakozni kívánt szoba nevét (vagy teljes URL-címét). Megadhat egy nevet, csak tudassa másokkal, hogy ugyanazt a nevet írják be.", 19 | "settingsDrawerButton": "Kattintson ide a beállítások fiókjának megnyitásához.", 20 | "serverSetting": "Kiszolgáló, ahol a konferenciákat tartják. Használja saját kiszolgálóját; ez azonban nem szükséges.", 21 | "serverTimeout": "Ha a résztvevők nem csatlakoztak az időkorlát bekövetkezése előtt, akkor a találkozót visszavonják.", 22 | "alwaysOnTop": "A „Mindig látható” ablak lehetővé teszi a be-/kikapcsolást. Akkor jelenik meg, amikor a főablak elveszíti a fókuszt. Ezt minden konferenciára alkalmazni kell." 23 | }, 24 | "settings": { 25 | "back": "Vissza", 26 | "alwaysOnTopWindow": "Mindig látható", 27 | "invalidServer": "Érvénytelen kiszolgáló URL-címe", 28 | "invalidServerTimeout": "A kiszolgáló időkorlátja érvénytelen", 29 | "serverUrl": "Kiszolgáló URL-címe", 30 | "serverTimeout": "Kiszolgálói időkorlát (másodperc)", 31 | "disableAGC": "Önműködő erősítésvezérlés letiltása" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/i18n/lang/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Inserisci un numero di conferenza oppure un URL Jitsi", 3 | "go": "VAI", 4 | "help": "Aiuto", 5 | "termsLink": "Condizioni d'uso", 6 | "privacyLink": "Informazioni sulla privacy", 7 | "sendFeedbackLink": "Inviaci Feedback", 8 | "aboutLink": "FAQ", 9 | "sourceLink": "Codice sorgente", 10 | "versionLabel": "Versione: {{version}}", 11 | "onboarding": { 12 | "startTour": "Inizia Tour", 13 | "skip": "salta", 14 | "welcome": "Benvenuto in {{appName}}", 15 | "letUsShowYouAround": "Ti facciamo vedere come funziona tutto!", 16 | "next": "Avanti", 17 | "conferenceUrl": "Insecisci il nome (oppure l'url intero) della stanza in cui vuoi accedere.", 18 | "settingsDrawerButton": "Premi qua per arrivare alle impostazioni.", 19 | "serverSetting": "Questo è il server dove si svolgeranno tutte le conferenze, qua si può anche inserire il proprio server!", 20 | "serverTimeout": "Questo è il tempo massimo che un server può metterci per rispondere ad una richiesta di accesso a un meeting.", 21 | "alwaysOnTop": "Qua puoi attivare una piccola finestra che ti farà vedere i partecipanti del meeting anche se hai minimizzato la finestra principale." 22 | }, 23 | "settings": { 24 | "back": "Indietro", 25 | "alwaysOnTopWindow": "Finestra sempre in primo piano", 26 | "invalidServer": "URL del server errato", 27 | "serverUrl": "URL Server", 28 | "serverTimeout": "Timeout del server (in secondi)" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/i18n/lang/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Voer de naam van de vergadering of een Jitsi-URL in", 3 | "go": "Deelnemen", 4 | "help": "Help", 5 | "termsLink": "Voorwaarden", 6 | "privacyLink": "Privacy", 7 | "sendFeedbackLink": "Stuur feedback", 8 | "aboutLink": "Over", 9 | "sourceLink": "Broncode", 10 | "versionLabel": "Versie: {{version}}", 11 | "onboarding": { 12 | "startTour": "Start rondleiding", 13 | "skip": "Overslaan", 14 | "welcome": "Welkom bij {{appName}}", 15 | "letUsShowYouAround": "Start de rondleiding!", 16 | "next": "Volgende", 17 | "conferenceUrl": "Voer de naam of URL in van de ruimte die u wilt betreden. U kunt een naam verzinnen, maar laat deze wel weten aan de andere deelnemers, zodat zij dezelfde naam invoeren.", 18 | "settingsDrawerButton": "Klik hier om het instellingenscherm te tonen.", 19 | "serverSetting": "Voer hier de naam van de server in waar de vergaderingen plaatsvinden. U kunt uw eigen server gebruiken, maar dat hoeft niet!", 20 | "serverTimeout": "Voer hier de tijdslimiet in om de ruimte te betreden. Als de ruimte niet voor deze limiet betreden wordt, wordt de vergadering geannuleerd.", 21 | "alwaysOnTop": "Hiermee kunt u het altijd zichtbare venster aan- of uitzetten. Dit venster wordt getoond wanneer een andere applicatie actief wordt. Dit geldt voor alle vergaderingen." 22 | }, 23 | "settings": { 24 | "back": "Terug", 25 | "alwaysOnTopWindow": "Altijd zichtbare venster", 26 | "invalidServer": "Ongeldige server-URL", 27 | "invalidServerTimeout": "Ongeldige waarde voor server-tijdslimiet", 28 | "serverUrl": "Server-URL", 29 | "serverTimeout": "Server-tijdslimiet (in seconden)" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/i18n/lang/pt-br.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Digite um nome para a sua conferência ou uma Jitsi URL", 3 | "go": "Iniciar", 4 | "help": "Ajuda", 5 | "termsLink": "Termos", 6 | "privacyLink": "Privacidade", 7 | "sendFeedbackLink": "Enviar Comentário", 8 | "aboutLink": "Sobre", 9 | "sourceLink": "Código Fonte", 10 | "versionLabel": "Versão: {{version}}", 11 | "onboarding": { 12 | "startTour": "Começar a turnê", 13 | "skip": "Pular", 14 | "welcome": "Bem vindo ao {{appName}}", 15 | "letUsShowYouAround": "Deixe-nos mostrar a você!", 16 | "next": "Avançar", 17 | "conferenceUrl": "Digite o nome (ou URL completo) da sala em que deseja entrar. Você pode inventar um nome, apenas avise os outros para que insiram o mesmo nome.", 18 | "settingsDrawerButton": "Clique aqui para abrir a barra de configurações.", 19 | "serverSetting": "Este será o servidor onde suas conferências acontecerão. Você pode usar o seu próprio, mas não precisa!", 20 | "serverTimeout": "Tempo limite para entrar em uma reunião, se a reunião não tiver começado antes de atingir o tempo limite, ela será cancelada.", 21 | "alwaysOnTop": "Você pode alternar se deseja habilitar a janela \"sempre visível \", que é exibida quando a janela principal perde o foco. Isso será aplicado a todas as conferências." 22 | }, 23 | "settings": { 24 | "back": "Voltar", 25 | "alwaysOnTopWindow": "Janela Sempre Visível", 26 | "invalidServer": "URL do Servidor Inválida", 27 | "invalidServerTimeout": "Valor inválido para tempo limite do servidor", 28 | "serverUrl": "URL do servidor", 29 | "serverTimeout": "Tempo Limite do Servidor (em segundos)" 30 | } 31 | } -------------------------------------------------------------------------------- /app/i18n/lang/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Введите название для вашей конференции или URL-адрес приглашения Jitsi", 3 | "go": "Вперёд", 4 | "help": "Помощь", 5 | "termsLink": "Условия", 6 | "privacyLink": "Конфиденциальность", 7 | "sendFeedbackLink": "Отправить отзыв", 8 | "aboutLink": "О приложении", 9 | "sourceLink": "Исходный код", 10 | "versionLabel": "Версия: {{version}}", 11 | "onboarding": { 12 | "startTour": "Начать ознакомительный тур", 13 | "skip": "Пропустить", 14 | "welcome": "Добро пожаловать в {{appName}}", 15 | "letUsShowYouAround": "Мы покажем вам, как все работает!", 16 | "next": "Далее", 17 | "conferenceUrl": "Введите название (или полный URL-адрес) комнаты, к которой вы хотите присоединиться.", 18 | "settingsDrawerButton": "Нажмите здесь, чтобы перейти к настройкам.", 19 | "serverSetting": "Это сервер, на котором будут проходить все ваши конференции. Вы можете использовать свой собственный, но вам это не нужно!", 20 | "serverTimeout": "Timeout to join a meeting, if the meeting hasn't been joined before the timeout hits, it's cancelled.", 21 | "alwaysOnTop": "Здесь вы можете включить небольшое окно, которое показывает участников, даже если вы только что свернули основное приложение." 22 | }, 23 | "settings": { 24 | "back": "Назад", 25 | "alwaysOnTopWindow": "Поверх всех окон", 26 | "invalidServer": "Неверный URL-адрес сервера", 27 | "serverUrl": "URL-адрес сервера", 28 | "serverTimeout": "Тайм-аут сервера (в секундах)" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/i18n/lang/sq.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "Jepni një emër për konferencën tuaj, ose një URL Jitsi", 3 | "go": "SHKO", 4 | "help": "Ndihmë", 5 | "termsLink": "Kushte", 6 | "privacyLink": "Privatësi", 7 | "recentListLabel": "ose rihyni në një nga dhomat tuaja përdorur së fundi", 8 | "sendFeedbackLink": "Dërgoni Përshtypje", 9 | "aboutLink": "Mbi", 10 | "sourceLink": "Kod Burim", 11 | "versionLabel": "Version: {{version}}", 12 | "onboarding": { 13 | "startTour": "Nisni Turin", 14 | "skip": "Anashkaloje", 15 | "welcome": "Mirë se vini në {{appName}}", 16 | "letUsShowYouAround": "Le t’ju tregojmë gjërat!", 17 | "next": "Pasuesi", 18 | "conferenceUrl": "Jepni emrin (ose URL-në) e plotë të dhomës ku doni të hyni. Një emër mund ta sajoni, thjesht bëjuani të ditur të tjerëve, që të japin të njëjtin emër.", 19 | "settingsDrawerButton": "Klikoni këtu që të hapet sirtari i rregullimeve.", 20 | "serverSetting": "Ky do të jetë shërbyesi ku do të zhvillohen konferencat tuaja. Mund të përdorni tuajën, por jo domosdoshmërisht!", 21 | "serverTimeout": "Mbarim kohe për provë hyrjeje në një takim, nëse te takimi s’hyhet para se të skadojë kjo kohë, hyrja anulohet.", 22 | "alwaysOnTop": "Me këtë mund të zgjidhni nëse doni apo jo aktivizimin e dritares \"përherë-sipër\", që shfaqet kur fokusi, nga dritarja kryesore, kalon gjetkë. Kjo do të zbatohet për krejt konferencat." 23 | }, 24 | "settings": { 25 | "back": "Mbrapsht", 26 | "alwaysOnTopWindow": "Dritare Përherë Sipër", 27 | "invalidServer": "URL e Pavlefshme Shërbyesi", 28 | "invalidServerTimeout": "Vlerë e pavlefshme për Mbarim Kohe Shërbyesi", 29 | "serverUrl": "URL Shërbyesi", 30 | "serverTimeout": "Mbarim Kohe Shërbyesi (në sekonda)", 31 | "disableAGC": "Çaktivizoni kontroll të automatizuar gain-i" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/i18n/lang/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "输入你的会议室名称或Jitsi网址", 3 | "go": "开始", 4 | "help": "帮助", 5 | "termsLink": "条款", 6 | "privacyLink": "隐私", 7 | "recentListLabel": "或重新加入最近的会议", 8 | "sendFeedbackLink": "发送反馈", 9 | "aboutLink": "关于", 10 | "sourceLink": "源代码", 11 | "versionLabel": "版本:{{version}}", 12 | "onboarding": { 13 | "startTour": "开始参观", 14 | "skip": "跳过", 15 | "welcome": "欢迎使用{{appName}}", 16 | "letUsShowYouAround": "让我们带你参观一下!", 17 | "next": "下一个", 18 | "conferenceUrl": "输入你想加入的会议室的名称(或完整的网址),你也可以使用不同的名称创建会议室,其他人只需输入相同的名称即可加入。", 19 | "settingsDrawerButton": "点击此处打开设置。", 20 | "serverSetting": "这将是你开始会议的服务器,你可以使用自己的服务器,但你无需这样做!", 21 | "serverTimeout": "加入会议超时,如果在超时之前未加入会议,则会议将被取消。", 22 | "alwaysOnTop": "你可以选择是否要启用“始终置顶”,该窗口在主窗口失去焦点时显示,这将适用于所有会议。" 23 | }, 24 | "settings": { 25 | "back": "返回", 26 | "alwaysOnTopWindow": "始终置顶", 27 | "invalidServer": "服务器网址无效", 28 | "invalidServerTimeout": "服务器超时值无效", 29 | "serverUrl": "服务器网址", 30 | "serverTimeout": "服务器超时(秒)", 31 | "disableAGC": "禁用自动增益控制" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/i18n/lang/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "enterConferenceNameOrUrl": "輸入會議室名稱或 Jitsi 網址", 3 | "go": "開始", 4 | "help": "協助", 5 | "termsLink": "條款", 6 | "privacyLink": "隱私權", 7 | "recentListLabel": "或重新加入最近參加過的會議", 8 | "sendFeedbackLink": "傳送意見反應", 9 | "aboutLink": "關於", 10 | "sourceLink": "原始碼", 11 | "versionLabel": "版本:{{version}}", 12 | "onboarding": { 13 | "startTour": "開始導覽", 14 | "skip": "跳過", 15 | "welcome": "歡迎使用 {{appName}}", 16 | "letUsShowYouAround": "讓我們帶您四處逛逛!", 17 | "next": "下一步", 18 | "conferenceUrl": "請輸入您想加入的會議室名稱(或完整網址),您可以想一個名稱來建立會議室,只要其他人輸入相同的名稱就能加入會議室喔。", 19 | "settingsDrawerButton": "按一下這裡開啟設定。", 20 | "serverSetting": "這將是您開始會議的伺服器,您可以使用自己的伺服器,但這不是必需的!", 21 | "serverTimeout": "加入會議的逾時設定,如果在逾時之前沒有加入會議,則會議將被取消。", 22 | "alwaysOnTop": "您可以選擇是否啟用「保持在最上層」,該視窗將在主視窗失去焦點時顯示,這將適用於所有會議。" 23 | }, 24 | "settings": { 25 | "back": "返回", 26 | "alwaysOnTopWindow": "保持在最上層", 27 | "invalidServer": "伺服器網址無效", 28 | "invalidServerTimeout": "伺服器逾時值無效", 29 | "serverUrl": "伺服器網址", 30 | "serverTimeout": "伺服器逾時(秒)", 31 | "disableAGC": "停用自動增益控制" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/images/onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/jitsi-meet-electron/c2561fe39d19e4d39f0ce473faa61c97f2abf74f/app/images/onboarding.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * AtlasKit components will deflect from appearance if css-reset is not present. 5 | */ 6 | import '@atlaskit/css-reset'; 7 | 8 | import Spinner from '@atlaskit/spinner'; 9 | import { SpotlightManager } from '@atlaskit/onboarding'; 10 | 11 | import React, { Component, Suspense } from 'react'; 12 | import { render } from 'react-dom'; 13 | import { Provider } from 'react-redux'; 14 | import { PersistGate } from 'redux-persist/integration/react'; 15 | 16 | import { App } from './features/app'; 17 | import { persistor, store } from './features/redux'; 18 | 19 | import './i18n'; 20 | 21 | /** 22 | * Component encapsulating App component with redux store using provider. 23 | */ 24 | class Root extends Component<*> { 25 | /** 26 | * Implements React's {@link Component#render()}. 27 | * 28 | * @returns {ReactElement} 29 | */ 30 | render() { 31 | return ( 32 | 33 | 36 | 37 | } > 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | } 46 | 47 | /** 48 | * Render the main / root application. 49 | * 50 | * $FlowFixMe. 51 | */ 52 | render(, document.getElementById('app')); 53 | -------------------------------------------------------------------------------- /app/preload/preload.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | const { RemoteControl, 3 | setupScreenSharingRender, 4 | setupAlwaysOnTopRender, 5 | initPopupsConfigurationRender, 6 | setupPowerMonitorRender 7 | } = require('@jitsi/electron-sdk'); 8 | 9 | const whitelistedIpcChannels = [ 'protocol-data-msg', 'renderer-ready' ]; 10 | 11 | /** 12 | * Open an external URL. 13 | * 14 | * @param {string} url - The URL we with to open. 15 | * @returns {void} 16 | */ 17 | function openExternalLink(url) { 18 | ipcRenderer.send('jitsi-open-url', url); 19 | } 20 | 21 | /** 22 | * Setup the renderer process. 23 | * 24 | * @param {*} api - API object. 25 | * @param {*} options - Options for what to enable. 26 | * @returns {void} 27 | */ 28 | function setupRenderer(api, options = {}) { 29 | initPopupsConfigurationRender(api); 30 | 31 | const iframe = api.getIFrame(); 32 | 33 | setupScreenSharingRender(api); 34 | 35 | if (options.enableRemoteControl) { 36 | new RemoteControl(iframe); // eslint-disable-line no-new 37 | } 38 | 39 | // Allow window to be on top if enabled in settings 40 | if (options.enableAlwaysOnTopWindow) { 41 | setupAlwaysOnTopRender(api, null, { showOnPrejoin: true }); 42 | } 43 | 44 | setupPowerMonitorRender(api); 45 | } 46 | 47 | window.jitsiNodeAPI = { 48 | openExternalLink, 49 | setupRenderer, 50 | ipc: { 51 | on: (channel, listener) => { 52 | if (!whitelistedIpcChannels.includes(channel)) { 53 | return; 54 | } 55 | 56 | return ipcRenderer.on(channel, listener); 57 | }, 58 | send: channel => { 59 | if (!whitelistedIpcChannels.includes(channel)) { 60 | return; 61 | } 62 | 63 | return ipcRenderer.send(channel); 64 | }, 65 | removeListener: (channel, listener) => { 66 | if (!whitelistedIpcChannels.includes(channel)) { 67 | return; 68 | } 69 | 70 | return ipcRenderer.removeListener(channel, listener); 71 | } 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* global __dirname */ 2 | 3 | const { 4 | BrowserWindow, 5 | Menu, 6 | app, 7 | ipcMain 8 | } = require('electron'); 9 | const contextMenu = require('electron-context-menu'); 10 | const debug = require('electron-debug'); 11 | const isDev = require('electron-is-dev'); 12 | const { autoUpdater } = require('electron-updater'); 13 | const windowStateKeeper = require('electron-window-state'); 14 | const { 15 | initPopupsConfigurationMain, 16 | getPopupTarget, 17 | RemoteControlMain, 18 | setupAlwaysOnTopMain, 19 | setupPowerMonitorMain, 20 | setupScreenSharingMain 21 | } = require('@jitsi/electron-sdk'); 22 | const path = require('path'); 23 | const process = require('process'); 24 | const URL = require('url'); 25 | const config = require('./app/features/config'); 26 | const { openExternalLink } = require('./app/features/utils/openExternalLink'); 27 | const pkgJson = require('./package.json'); 28 | 29 | const showDevTools = Boolean(process.env.SHOW_DEV_TOOLS) || (process.argv.indexOf('--show-dev-tools') > -1); 30 | 31 | // For enabling remote control, please change the ENABLE_REMOTE_CONTROL flag in 32 | // app/features/conference/components/Conference.js to true as well 33 | const ENABLE_REMOTE_CONTROL = false; 34 | 35 | // We need this because of https://github.com/electron/electron/issues/18214 36 | app.commandLine.appendSwitch('disable-site-isolation-trials'); 37 | 38 | // Fix screen-sharing thumbnails being missing sometimes. 39 | // https://github.com/electron/electron/issues/44504 40 | const disabledFeatures = [ 41 | 'ThumbnailCapturerMac:capture_mode/sc_screenshot_manager', 42 | 'ScreenCaptureKitPickerScreen', 43 | 'ScreenCaptureKitStreamPickerSonoma' 44 | ]; 45 | 46 | app.commandLine.appendSwitch('disable-features', disabledFeatures.join(',')); 47 | 48 | // Enable Opus RED field trial. 49 | app.commandLine.appendSwitch('force-fieldtrials', 'WebRTC-Audio-Red-For-Opus/Enabled/'); 50 | 51 | // Wayland: Enable optional PipeWire support. 52 | if (!app.commandLine.hasSwitch('enable-features')) { 53 | app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer'); 54 | } 55 | 56 | autoUpdater.logger = require('electron-log'); 57 | autoUpdater.logger.transports.file.level = 'info'; 58 | 59 | // Enable context menu so things like copy and paste work in input fields. 60 | contextMenu({ 61 | showLookUpSelection: false, 62 | showSearchWithGoogle: false, 63 | showCopyImage: false, 64 | showCopyImageAddress: false, 65 | showSaveImage: false, 66 | showSaveImageAs: false, 67 | showInspectElement: true, 68 | showServices: false 69 | }); 70 | 71 | // Enable DevTools also on release builds to help troubleshoot issues. Don't 72 | // show them automatically though. 73 | debug({ 74 | isEnabled: true, 75 | showDevTools 76 | }); 77 | 78 | /** 79 | * When in development mode: 80 | * - Enable automatic reloads 81 | */ 82 | if (isDev) { 83 | require('electron-reload')(path.join(__dirname, 'build')); 84 | } 85 | 86 | /** 87 | * The window object that will load the iframe with Jitsi Meet. 88 | * IMPORTANT: Must be defined as global in order to not be garbage collected 89 | * acidentally. 90 | */ 91 | let mainWindow = null; 92 | 93 | let webrtcInternalsWindow = null; 94 | 95 | /** 96 | * Add protocol data 97 | */ 98 | const appProtocolSurplus = `${config.default.appProtocolPrefix}://`; 99 | let rendererReady = false; 100 | let protocolDataForFrontApp = null; 101 | 102 | 103 | /** 104 | * Sets the application menu. It is hidden on all platforms except macOS because 105 | * otherwise copy and paste functionality is not available. 106 | */ 107 | function setApplicationMenu() { 108 | if (process.platform === 'darwin') { 109 | const template = [ { 110 | label: app.name, 111 | submenu: [ 112 | { 113 | role: 'services', 114 | submenu: [] 115 | }, 116 | { type: 'separator' }, 117 | { role: 'hide' }, 118 | { role: 'hideothers' }, 119 | { role: 'unhide' }, 120 | { type: 'separator' }, 121 | { role: 'quit' } 122 | ] 123 | }, { 124 | label: 'Edit', 125 | submenu: [ { 126 | label: 'Undo', 127 | accelerator: 'CmdOrCtrl+Z', 128 | selector: 'undo:' 129 | }, 130 | { 131 | label: 'Redo', 132 | accelerator: 'Shift+CmdOrCtrl+Z', 133 | selector: 'redo:' 134 | }, 135 | { 136 | type: 'separator' 137 | }, 138 | { 139 | label: 'Cut', 140 | accelerator: 'CmdOrCtrl+X', 141 | selector: 'cut:' 142 | }, 143 | { 144 | label: 'Copy', 145 | accelerator: 'CmdOrCtrl+C', 146 | selector: 'copy:' 147 | }, 148 | { 149 | label: 'Paste', 150 | accelerator: 'CmdOrCtrl+V', 151 | selector: 'paste:' 152 | }, 153 | { 154 | label: 'Select All', 155 | accelerator: 'CmdOrCtrl+A', 156 | selector: 'selectAll:' 157 | } ] 158 | }, { 159 | label: '&Window', 160 | role: 'window', 161 | submenu: [ 162 | { role: 'minimize' }, 163 | { role: 'close' } 164 | ] 165 | } ]; 166 | 167 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)); 168 | } else { 169 | Menu.setApplicationMenu(null); 170 | } 171 | } 172 | 173 | /** 174 | * Opens new window with index.html(Jitsi Meet is loaded in iframe there). 175 | */ 176 | function createJitsiMeetWindow() { 177 | // Application menu. 178 | setApplicationMenu(); 179 | 180 | // Check for Updates. 181 | if (!process.mas) { 182 | autoUpdater.checkForUpdatesAndNotify(); 183 | } 184 | 185 | // Load the previous window state with fallback to defaults. 186 | const windowState = windowStateKeeper({ 187 | defaultWidth: 800, 188 | defaultHeight: 600, 189 | fullScreen: false 190 | }); 191 | 192 | // Path to root directory. 193 | const basePath = isDev ? __dirname : app.getAppPath(); 194 | 195 | // URL for index.html which will be our entry point. 196 | const indexURL = URL.format({ 197 | pathname: path.resolve(basePath, './build/index.html'), 198 | protocol: 'file:', 199 | slashes: true 200 | }); 201 | 202 | // Options used when creating the main Jitsi Meet window. 203 | // Use a preload script in order to provide node specific functionality 204 | // to a isolated BrowserWindow in accordance with electron security 205 | // guideline. 206 | const options = { 207 | x: windowState.x, 208 | y: windowState.y, 209 | width: windowState.width, 210 | height: windowState.height, 211 | icon: path.resolve(basePath, './resources/icon.png'), 212 | minWidth: 800, 213 | minHeight: 600, 214 | show: false, 215 | webPreferences: { 216 | enableBlinkFeatures: 'WebAssemblyCSP', 217 | contextIsolation: false, 218 | nodeIntegration: false, 219 | preload: path.resolve(basePath, './build/preload.js'), 220 | sandbox: false 221 | } 222 | }; 223 | 224 | const windowOpenHandler = ({ url, frameName }) => { 225 | const target = getPopupTarget(url, frameName); 226 | 227 | if (!target || target === 'browser') { 228 | openExternalLink(url); 229 | 230 | return { action: 'deny' }; 231 | } 232 | 233 | if (target === 'electron') { 234 | return { action: 'allow' }; 235 | } 236 | 237 | return { action: 'deny' }; 238 | }; 239 | 240 | mainWindow = new BrowserWindow(options); 241 | windowState.manage(mainWindow); 242 | mainWindow.loadURL(indexURL); 243 | 244 | mainWindow.webContents.setWindowOpenHandler(windowOpenHandler); 245 | 246 | if (isDev) { 247 | mainWindow.webContents.session.clearCache(); 248 | } 249 | 250 | // Block access to file:// URLs. 251 | const fileFilter = { 252 | urls: [ 'file://*' ] 253 | }; 254 | 255 | mainWindow.webContents.session.webRequest.onBeforeSendHeaders(fileFilter, (details, callback) => { 256 | const requestedPath = path.resolve(URL.fileURLToPath(details.url)); 257 | const appBasePath = path.resolve(basePath); 258 | 259 | if (!requestedPath.startsWith(appBasePath)) { 260 | callback({ cancel: true }); 261 | console.warn(`Rejected file URL: ${details.url}`); 262 | 263 | return; 264 | } 265 | 266 | callback({ cancel: false }); 267 | }); 268 | 269 | // Filter out x-frame-options and frame-ancestors CSP to allow loading jitsi via the iframe API 270 | // Resolves https://github.com/jitsi/jitsi-meet-electron/issues/285 271 | mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => { 272 | delete details.responseHeaders['x-frame-options']; 273 | 274 | if (details.responseHeaders['content-security-policy']) { 275 | const cspFiltered = details.responseHeaders['content-security-policy'][0] 276 | .split(';') 277 | .filter(x => x.indexOf('frame-ancestors') === -1) 278 | .join(';'); 279 | 280 | details.responseHeaders['content-security-policy'] = [ cspFiltered ]; 281 | } 282 | 283 | if (details.responseHeaders['Content-Security-Policy']) { 284 | const cspFiltered = details.responseHeaders['Content-Security-Policy'][0] 285 | .split(';') 286 | .filter(x => x.indexOf('frame-ancestors') === -1) 287 | .join(';'); 288 | 289 | details.responseHeaders['Content-Security-Policy'] = [ cspFiltered ]; 290 | } 291 | 292 | callback({ 293 | responseHeaders: details.responseHeaders 294 | }); 295 | }); 296 | 297 | // Block redirects. 298 | const allowedRedirects = [ 299 | 'http:', 300 | 'https:', 301 | 'ws:', 302 | 'wss:' 303 | ]; 304 | 305 | mainWindow.webContents.addListener('will-redirect', (ev, url) => { 306 | const requestedUrl = new URL.URL(url); 307 | 308 | if (!allowedRedirects.includes(requestedUrl.protocol)) { 309 | console.warn(`Disallowing redirect to ${url}`); 310 | ev.preventDefault(); 311 | } 312 | }); 313 | 314 | // Block opening any external applications. 315 | mainWindow.webContents.session.setPermissionRequestHandler((_, permission, callback, details) => { 316 | if (permission === 'openExternal') { 317 | console.warn(`Disallowing opening ${details.externalURL}`); 318 | callback(false); 319 | 320 | return; 321 | } 322 | 323 | callback(true); 324 | }); 325 | 326 | initPopupsConfigurationMain(mainWindow); 327 | setupAlwaysOnTopMain(mainWindow, null, windowOpenHandler); 328 | setupPowerMonitorMain(mainWindow); 329 | setupScreenSharingMain(mainWindow, config.default.appName, pkgJson.build.appId); 330 | if (ENABLE_REMOTE_CONTROL) { 331 | new RemoteControlMain(mainWindow); // eslint-disable-line no-new 332 | } 333 | 334 | mainWindow.on('closed', () => { 335 | mainWindow = null; 336 | }); 337 | mainWindow.once('ready-to-show', () => { 338 | mainWindow.show(); 339 | }); 340 | 341 | /** 342 | * When someone tries to enter something like jitsi-meet://test 343 | * while app is closed 344 | * it will trigger this event below 345 | */ 346 | handleProtocolCall(process.argv.pop()); 347 | } 348 | 349 | /** 350 | * Opens new window with WebRTC internals. 351 | */ 352 | function createWebRTCInternalsWindow() { 353 | const options = { 354 | minWidth: 800, 355 | minHeight: 600, 356 | show: true 357 | }; 358 | 359 | webrtcInternalsWindow = new BrowserWindow(options); 360 | webrtcInternalsWindow.loadURL('chrome://webrtc-internals'); 361 | } 362 | 363 | /** 364 | * Handler for application protocol links to initiate a conference. 365 | */ 366 | function handleProtocolCall(fullProtocolCall) { 367 | // don't touch when something is bad 368 | if ( 369 | !fullProtocolCall 370 | || fullProtocolCall.trim() === '' 371 | || fullProtocolCall.indexOf(appProtocolSurplus) !== 0 372 | ) { 373 | return; 374 | } 375 | 376 | const inputURL = fullProtocolCall.replace(appProtocolSurplus, ''); 377 | 378 | if (app.isReady() && mainWindow === null) { 379 | createJitsiMeetWindow(); 380 | } 381 | 382 | protocolDataForFrontApp = inputURL; 383 | 384 | if (rendererReady) { 385 | mainWindow 386 | .webContents 387 | .send('protocol-data-msg', inputURL); 388 | } 389 | } 390 | 391 | /** 392 | * Force Single Instance Application. 393 | * Handle this on darwin via LSMultipleInstancesProhibited in Info.plist as below does not work on MAS 394 | */ 395 | const gotInstanceLock = process.platform === 'darwin' ? true : app.requestSingleInstanceLock(); 396 | 397 | if (!gotInstanceLock) { 398 | app.quit(); 399 | process.exit(0); 400 | } 401 | 402 | /** 403 | * Run the application. 404 | */ 405 | 406 | app.on('activate', () => { 407 | if (mainWindow === null) { 408 | createJitsiMeetWindow(); 409 | } 410 | }); 411 | 412 | app.on('certificate-error', 413 | // eslint-disable-next-line max-params 414 | (event, webContents, url, error, certificate, callback) => { 415 | if (isDev) { 416 | event.preventDefault(); 417 | callback(true); 418 | } else { 419 | callback(false); 420 | } 421 | } 422 | ); 423 | 424 | app.on('ready', createJitsiMeetWindow); 425 | 426 | if (isDev) { 427 | app.on('ready', createWebRTCInternalsWindow); 428 | } 429 | 430 | app.on('second-instance', (event, commandLine) => { 431 | /** 432 | * If someone creates second instance of the application, set focus on 433 | * existing window. 434 | */ 435 | if (mainWindow) { 436 | mainWindow.isMinimized() && mainWindow.restore(); 437 | mainWindow.focus(); 438 | 439 | /** 440 | * This is for windows [win32] 441 | * so when someone tries to enter something like jitsi-meet://test 442 | * while app is opened it will trigger protocol handler. 443 | */ 444 | handleProtocolCall(commandLine.pop()); 445 | } 446 | }); 447 | 448 | app.on('window-all-closed', () => { 449 | app.quit(); 450 | }); 451 | 452 | // remove so we can register each time as we run the app. 453 | app.removeAsDefaultProtocolClient(config.default.appProtocolPrefix); 454 | 455 | // If we are running a non-packaged version of the app && on windows 456 | if (isDev && process.platform === 'win32') { 457 | // Set the path of electron.exe and your app. 458 | // These two additional parameters are only available on windows. 459 | app.setAsDefaultProtocolClient( 460 | config.default.appProtocolPrefix, 461 | process.execPath, 462 | [ path.resolve(process.argv[1]) ] 463 | ); 464 | } else { 465 | app.setAsDefaultProtocolClient(config.default.appProtocolPrefix); 466 | } 467 | 468 | /** 469 | * This is for mac [darwin] 470 | * so when someone tries to enter something like jitsi-meet://test 471 | * it will trigger this event below 472 | */ 473 | app.on('open-url', (event, data) => { 474 | event.preventDefault(); 475 | handleProtocolCall(data); 476 | }); 477 | 478 | /** 479 | * This is to notify main.js [this] that front app is ready to receive messages. 480 | */ 481 | ipcMain.on('renderer-ready', () => { 482 | rendererReady = true; 483 | if (protocolDataForFrontApp) { 484 | mainWindow 485 | .webContents 486 | .send('protocol-data-msg', protocolDataForFrontApp); 487 | } 488 | }); 489 | 490 | /** 491 | * Handle opening external links in the main process. 492 | */ 493 | ipcMain.on('jitsi-open-url', (event, someUrl) => { 494 | openExternalLink(someUrl); 495 | }); 496 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jitsi-meet-electron", 3 | "version": "2025.4.0", 4 | "description": "Electron application for Jitsi Meet", 5 | "main": "./build/main.js", 6 | "productName": "Jitsi Meet", 7 | "private": true, 8 | "scripts": { 9 | "start": "webpack --config ./webpack.main.js --mode development && concurrently \"npm:watch\" \"electron ./build/main.js\"", 10 | "clean": "rm -rf node_modules build dist", 11 | "lint": "eslint . && flow", 12 | "build": "webpack --config ./webpack.main.js --mode production && webpack --config ./webpack.renderer.js --mode production", 13 | "dist": "npm run build && electron-builder", 14 | "postinstall": "patch-package", 15 | "watch": "webpack --config ./webpack.renderer.js --mode development --watch --watch-options-poll=1000" 16 | }, 17 | "engines": { 18 | "node": ">=14.0.0" 19 | }, 20 | "build": { 21 | "appId": "org.jitsi.jitsi-meet", 22 | "productName": "Jitsi Meet", 23 | "generateUpdatesFilesForAllChannels": true, 24 | "npmRebuild": false, 25 | "files": [ 26 | "build", 27 | "resources", 28 | "!app", 29 | "!main.js" 30 | ], 31 | "mac": { 32 | "artifactName": "jitsi-meet.${ext}", 33 | "target": [ 34 | { 35 | "arch": "universal", 36 | "target": "dmg" 37 | }, 38 | { 39 | "arch": "universal", 40 | "target": "zip" 41 | } 42 | ], 43 | "category": "public.app-category.video", 44 | "darkModeSupport": true, 45 | "entitlements": "resources/entitlements.mac.plist", 46 | "extendInfo": { 47 | "NSCameraUsageDescription": "Jitsi Meet requires access to your camera in order to make video-calls.", 48 | "NSMicrophoneUsageDescription": "Jitsi Meet requires access to your microphone in order to make calls (audio/video).", 49 | "LSMultipleInstancesProhibited": true 50 | } 51 | }, 52 | "mas": { 53 | "entitlements": "resources/entitlements.mas.plist", 54 | "entitlementsInherit": "resources/entitlements.mas.inherit.plist", 55 | "hardenedRuntime": false 56 | }, 57 | "linux": { 58 | "artifactName": "jitsi-meet-${arch}.${ext}", 59 | "category": "VideoConference;AudioVideo;Audio;Video;Network", 60 | "description": "Jitsi Meet Desktop App", 61 | "executableName": "jitsi-meet", 62 | "target": [ 63 | { 64 | "arch": "x64", 65 | "target": "AppImage" 66 | }, 67 | { 68 | "arch": "x64", 69 | "target": "deb" 70 | }, 71 | { 72 | "arch": "arm64", 73 | "target": "AppImage" 74 | }, 75 | { 76 | "arch": "arm64", 77 | "target": "deb" 78 | } 79 | ] 80 | }, 81 | "win": { 82 | "artifactName": "jitsi-meet.${ext}", 83 | "target": [ 84 | { 85 | "arch": [ 86 | "ia32", 87 | "x64", 88 | "arm64" 89 | ], 90 | "target": "nsis" 91 | } 92 | ] 93 | }, 94 | "directories": { 95 | "buildResources": "resources" 96 | }, 97 | "protocols": [ 98 | { 99 | "name": "jitsi-protocol", 100 | "role": "Viewer", 101 | "schemes": [ 102 | "jitsi-meet" 103 | ] 104 | } 105 | ] 106 | }, 107 | "pre-commit": [ 108 | "lint" 109 | ], 110 | "repository": { 111 | "type": "git", 112 | "url": "git://github.com/jitsi/jitsi-meet-electron" 113 | }, 114 | "keywords": [ 115 | "jingle", 116 | "webrtc", 117 | "xmpp", 118 | "electron", 119 | "jitsi-meet" 120 | ], 121 | "author": "Jitsi Team ", 122 | "readmeFilename": "README.md", 123 | "license": "Apache-2.0", 124 | "dependencies": { 125 | "@jitsi/electron-sdk": "7.0.6", 126 | "electron-debug": "^3.2.0", 127 | "electron-reload": "^1.5.0" 128 | }, 129 | "devDependencies": { 130 | "@atlaskit/button": "^10.1.3", 131 | "@atlaskit/css-reset": "^3.0.8", 132 | "@atlaskit/droplist": "^7.0.19", 133 | "@atlaskit/field-text": "^7.1.0", 134 | "@atlaskit/icon": "^15.0.3", 135 | "@atlaskit/navigation": "^33.3.10", 136 | "@atlaskit/onboarding": "^6.2.0", 137 | "@atlaskit/page": "^8.0.12", 138 | "@atlaskit/spinner": "^9.0.13", 139 | "@atlaskit/theme": "^7.0.5", 140 | "@atlaskit/toggle": "^5.0.15", 141 | "@babel/core": "^7.17.8", 142 | "@babel/plugin-proposal-class-properties": "^7.16.7", 143 | "@babel/plugin-proposal-export-namespace-from": "^7.16.7", 144 | "@babel/plugin-transform-flow-strip-types": "^7.16.7", 145 | "@babel/preset-env": "^7.16.11", 146 | "@babel/preset-flow": "^7.16.7", 147 | "@babel/preset-react": "^7.16.7", 148 | "@hapi/bourne": "^3.0.0", 149 | "@jitsi/js-utils": "2.0.5", 150 | "@svgr/webpack": "^6.2.1", 151 | "babel-eslint": "10.0.3", 152 | "babel-loader": "^8.2.3", 153 | "concurrently": "5.1.0", 154 | "css-loader": "^6.7.1", 155 | "electron": "35.1.5", 156 | "electron-builder": "26.0.12", 157 | "electron-context-menu": "^2.5.0", 158 | "electron-is-dev": "^1.2.0", 159 | "electron-log": "^4.3.2", 160 | "electron-react-devtools": "0.5.3", 161 | "electron-updater": "6.6.2", 162 | "electron-window-state": "^5.0.3", 163 | "eslint": "6.5.1", 164 | "eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#1.0.2", 165 | "eslint-plugin-flowtype": "4.7.0", 166 | "eslint-plugin-import": "2.20.2", 167 | "eslint-plugin-jsdoc": "22.1.0", 168 | "eslint-plugin-react": "7.19.0", 169 | "file-loader": "^6.2.0", 170 | "flow-bin": "0.109.0", 171 | "history": "^4.10.1", 172 | "html-webpack-plugin": "^5.5.0", 173 | "i18next": "^19.9.2", 174 | "moment": "^2.29.2", 175 | "mousetrap": "^1.6.5", 176 | "patch-package": "6.5.1", 177 | "react": "^16.14.0", 178 | "react-dom": "^16.14.0", 179 | "react-i18next": "^11.8.12", 180 | "react-redux": "^5.1.2", 181 | "react-router-redux": "^5.0.0-alpha.9", 182 | "redux": "^4.1.2", 183 | "redux-logger": "^3.0.6", 184 | "redux-persist": "^5.10.0", 185 | "source-map-support": "^0.5.19", 186 | "style-loader": "^3.3.1", 187 | "styled-components": "^3.4.10", 188 | "webpack": "^5.94.0", 189 | "webpack-cli": "^4.9.2" 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /patches/electron-reload+1.5.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/electron-reload/main.js b/node_modules/electron-reload/main.js 2 | index 688934d..860e071 100644 3 | --- a/node_modules/electron-reload/main.js 4 | +++ b/node_modules/electron-reload/main.js 5 | @@ -9,7 +9,7 @@ const ignoredPaths = /node_modules|[/\\]\./ 6 | // only effective when the process is restarted (hard reset) 7 | // We assume that electron-reload is required by the main 8 | // file of the electron application 9 | -const mainFile = module.parent.filename 10 | +const mainFile = 'build/main.js' 11 | 12 | /** 13 | * Creates a callback for hard resets. 14 | -------------------------------------------------------------------------------- /resources/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.device.camera 8 | 9 | com.apple.security.device.audio-input 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/entitlements.mas.inherit.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.inherit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/entitlements.mas.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | com.apple.security.network.server 10 | 11 | com.apple.security.device.camera 12 | 13 | com.apple.security.device.microphone 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/jitsi-meet-electron/c2561fe39d19e4d39f0ce473faa61c97f2abf74f/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/jitsi-meet-electron/c2561fe39d19e4d39f0ce473faa61c97f2abf74f/resources/icon.png -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/jitsi-meet-electron/c2561fe39d19e4d39f0ce473faa61c97f2abf74f/resources/icons/512x512.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/jitsi-meet-electron/c2561fe39d19e4d39f0ce473faa61c97f2abf74f/screenshot.png -------------------------------------------------------------------------------- /screenshot_linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/jitsi-meet-electron/c2561fe39d19e4d39f0ce473faa61c97f2abf74f/screenshot_linux.png -------------------------------------------------------------------------------- /webpack.main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | target: 'electron-main', 5 | entry: { main: './main.js', 6 | preload: './app/preload/preload.js' }, 7 | output: { 8 | path: path.resolve('./build'), 9 | filename: '[name].js' 10 | }, 11 | node: { 12 | __dirname: true 13 | }, 14 | externals: [ { 15 | '@jitsi/electron-sdk': 'require(\'@jitsi/electron-sdk\')', 16 | 'electron-debug': 'require(\'electron-debug\')', 17 | 'electron-reload': 'require(\'electron-reload\')' 18 | } ], 19 | resolve: { 20 | modules: [ 21 | path.resolve('./node_modules') 22 | ] 23 | } 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /webpack.renderer.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const ELECTRON_VERSION = require('./package.json').devDependencies.electron; 4 | 5 | module.exports = { 6 | // The renderer code rus in BrowserWindow without node support so we must 7 | // target a web platform. 8 | target: 'web', 9 | entry: { app: './app/index.js' }, 10 | performance: { 11 | maxAssetSize: 1.5 * 1024 * 1024, 12 | maxEntrypointSize: 1.5 * 1024 * 1024 13 | }, 14 | plugins: [ 15 | new HtmlWebpackPlugin({ 16 | template: './app/index.html' 17 | }) 18 | ], 19 | output: { 20 | path: path.resolve('./build'), 21 | filename: '[name].js' 22 | }, 23 | node: { 24 | __dirname: true 25 | }, 26 | module: { 27 | noParse: /external_api\\.js/, 28 | rules: [ 29 | { 30 | loader: 'babel-loader', 31 | options: { 32 | babelrc: false, 33 | presets: [ 34 | [ 35 | require.resolve('@babel/preset-env'), 36 | { 37 | modules: false, 38 | targets: { 39 | electron: ELECTRON_VERSION 40 | } 41 | } 42 | ], 43 | require.resolve('@babel/preset-flow'), 44 | require.resolve('@babel/preset-react') 45 | ], 46 | plugins: [ 47 | require.resolve('@babel/plugin-transform-flow-strip-types'), 48 | require.resolve('@babel/plugin-proposal-class-properties'), 49 | require.resolve('@babel/plugin-proposal-export-namespace-from') 50 | ] 51 | }, 52 | test: /\.js$/ 53 | }, 54 | { 55 | use: [ 56 | { loader: 'style-loader' }, 57 | { loader: 'css-loader' } 58 | ], 59 | test: /\.css$/ 60 | }, 61 | { 62 | use: 'file-loader', 63 | test: /\.png$/ 64 | }, 65 | { 66 | test: /\.svg$/, 67 | use: [ { 68 | loader: '@svgr/webpack', 69 | options: { 70 | dimensions: false, 71 | expandProps: 'start' 72 | } 73 | } ] 74 | } 75 | ] 76 | }, 77 | externals: [ { 78 | '@jitsi/electron-sdk': 'require(\'@jitsi/electron-sdk\')' 79 | } ], 80 | resolve: { 81 | modules: [ 82 | path.resolve('./node_modules') 83 | ] 84 | } 85 | }; 86 | 87 | --------------------------------------------------------------------------------