├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── NOTICE ├── README.md ├── app ├── .testcafe-electron-rc ├── Routes.tsx ├── app.global.css ├── app.html ├── app.icns ├── chime │ └── ChimeSdkWrapper.ts ├── components │ ├── App.tsx │ ├── CPUUsage.css │ ├── CPUUsage.tsx │ ├── Chat.css │ ├── Chat.tsx │ ├── ChatInput.css │ ├── ChatInput.tsx │ ├── Classroom.css │ ├── Classroom.tsx │ ├── ContentVideo.css │ ├── ContentVideo.tsx │ ├── Controls.css │ ├── Controls.tsx │ ├── CreateOrJoin.css │ ├── CreateOrJoin.tsx │ ├── DeviceSwitcher.css │ ├── DeviceSwitcher.tsx │ ├── Error.css │ ├── Error.tsx │ ├── Home.css │ ├── Home.tsx │ ├── LoadingSpinner.css │ ├── LoadingSpinner.tsx │ ├── LocalVideo.css │ ├── LocalVideo.tsx │ ├── Login.css │ ├── Login.tsx │ ├── RemoteVideo.css │ ├── RemoteVideo.tsx │ ├── RemoteVideoGroup.css │ ├── RemoteVideoGroup.tsx │ ├── Root.tsx │ ├── Roster.css │ ├── Roster.tsx │ ├── ScreenPicker.css │ ├── ScreenPicker.tsx │ ├── ScreenShareHeader.css │ ├── ScreenShareHeader.tsx │ ├── Tooltip.css │ ├── Tooltip.tsx │ ├── VideoNameplate.css │ ├── VideoNameplate.tsx │ └── css.d.ts ├── constants │ ├── localStorageKeys.json │ └── routes.json ├── context │ ├── getChimeContext.ts │ ├── getMeetingStatusContext.ts │ ├── getRosterContext.ts │ └── getUIStateContext.ts ├── enums │ ├── ClassMode.ts │ ├── MeetingStatus.ts │ ├── MessageTopic.ts │ ├── OptionalFeature.ts │ ├── Size.ts │ └── ViewMode.ts ├── hooks │ ├── useAttendee.tsx │ ├── useDevices.tsx │ ├── useFocusMode.tsx │ ├── useRaisedHandAttendees.tsx │ └── useRoster.tsx ├── i18n │ └── en-US.ts ├── index.tsx ├── main.dev.ts ├── main.prod.js.LICENSE ├── main.prod.js.LICENSE.txt ├── menu.ts ├── package.json ├── providers │ ├── ChimeProvider.tsx │ ├── I18nProvider.tsx │ ├── MeetingStatusProvider.tsx │ └── UIStateProvider.tsx ├── types │ ├── DeviceType.ts │ ├── FullDeviceInfoType.ts │ ├── MessageUpdateCallbackType.ts │ ├── RegionType.ts │ ├── RosterAttendeeType.ts │ └── RosterType.ts ├── utils │ └── .gitkeep └── yarn.lock ├── babel.config.js ├── configs ├── .eslintrc ├── webpack.config.base.js ├── webpack.config.eslint.js ├── webpack.config.main.prod.babel.js ├── webpack.config.renderer.dev.babel.js ├── webpack.config.renderer.dev.dll.babel.js └── webpack.config.renderer.prod.babel.js ├── internals ├── mocks │ └── fileMock.js └── scripts │ ├── .eslintrc │ ├── BabelRegister.js │ ├── CheckBuildsExist.js │ ├── CheckNativeDep.js │ ├── CheckNodeEnv.js │ ├── CheckPortInUse.js │ ├── CheckYarn.js │ ├── DeleteSourceMaps.js │ └── ElectronRebuild.js ├── package-lock.json ├── package.json ├── resources ├── download.css ├── icon.icns ├── icon.ico ├── icon.png ├── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png └── readme-hero.jpg ├── script ├── cloud9-resize.sh └── deploy.js ├── serverless ├── .gitignore ├── src │ ├── handlers.js │ ├── index.js │ └── messaging.js └── template.yaml ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # App packaged 34 | release 35 | app/main.prod.js 36 | app/main.prod.js.map 37 | app/renderer.prod.js 38 | app/renderer.prod.js.map 39 | app/style.css 40 | app/style.css.map 41 | dist 42 | dll 43 | main.js 44 | main.js.map 45 | 46 | .idea 47 | npm-debug.log.* 48 | __snapshots__ 49 | 50 | # Package.json 51 | package.json 52 | .travis.yml 53 | *.css.d.ts 54 | *.sass.d.ts 55 | *.scss.d.ts 56 | 57 | # Chime SDK 58 | amazon-chime-sdk-js 59 | serverless 60 | script 61 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb/typescript', 3 | rules: { 4 | // A temporary hack related to IDE not resolving correct package.json 5 | 'import/no-extraneous-dependencies': 'off' 6 | }, 7 | settings: { 8 | 'import/resolver': { 9 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 10 | node: {}, 11 | webpack: { 12 | config: require.resolve('./configs/webpack.config.eslint.js') 13 | } 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.png binary 3 | *.jpg binary 4 | *.jpeg binary 5 | *.ico binary 6 | *.icns binary 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Issue #:** 2 | 3 | **Description of changes:** 4 | 5 | **Testing** 6 | 7 | 1. How did you test these changes? 8 | 9 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # App packaged 34 | release 35 | app/main.prod.js 36 | app/main.prod.js.map 37 | app/renderer.prod.js 38 | app/renderer.prod.js.map 39 | app/style.css 40 | app/style.css.map 41 | dist 42 | dll 43 | main.js 44 | main.js.map 45 | 46 | .idea 47 | npm-debug.log.* 48 | *.css.d.ts 49 | *.sass.d.ts 50 | *.scss.d.ts 51 | 52 | # Chime SDK 53 | app/utils/getBaseUrl.ts 54 | app/utils/getMessagingWssUrl.ts 55 | serverless/build 56 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | amazon-chime-sdk-classroom-demo 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon Chime SDK Classroom Demo 2 | 3 | This demo shows how to use the Amazon Chime SDK to build an online classroom in Electron and React. This demo will ship two packages to the end user, one for windows and another one for mac. 4 | 5 | Amazon Chime SDK Classroom Demo 6 | 7 | ## Installation 8 | 9 | To package this demo, there are two options available. The first option is to package the demo via Cloud9 and the second option is to package from the local machine. 10 | 11 | The two options are very similar in their internal working. On a high level the following steps are performed in the packaging of the demo: 12 | 13 | 1. CLI arguments are parsed and the installation of prerequisites is verified. `yarn` is installed globally in the environment. 14 | 2. For installation option 1, Cloud9 volume size might be increased. 15 | 3. S3 buckets are created. 16 | 4. `sam` packages the application. 17 | 5. `sam` deploys the application. 18 | 6. The output of the AWS cloudformation stack is retrieved. 19 | 7. Using `electron-builder` mac application is packaged. 20 | 8. Using `electron-builder` windows application is packaged. 21 | 9. Generated packages are copied from local workspace / cloud9 environment to S3 buckets. 22 | 23 | At the end of the script run, a link to download the packaged demos will be displayed. Now, the teacher and the students can use that link to download the classroom application on their machine. 24 | 25 | ### Option 1: Deploy via AWS Cloud9 26 | 27 | #### Prerequisites 28 | 29 | - Log into your AWS account with an IAM role that has the **AdministratorAccess** policy. 30 | - Use the **us-east-1 (N. Virginia)** region of your AWS account. 31 | 32 | #### Create an AWS Cloud9 environment 33 | 34 | 1. Go to the [AWS Cloud9 Dashboard](https://us-east-1.console.aws.amazon.com/cloud9/home?region=us-east-1). 35 | 2. Press the **Create environment** button or go [here](https://us-east-1.console.aws.amazon.com/cloud9/home/create). 36 | 3. For the Name enter `` and press the **Next step** button. 37 | 4. For **Environment Settings** use the defaults and press the **Next step** button. 38 | 5. Review the **Environment name and settings** and press the **Create environment** button. 39 | 6. Wait for the environment to start. 40 | 41 | #### Run the deployment script 42 | 43 | Once the Cloud9 environment starts, run the following commands in the Terminal pane at the bottom of the window to download the application repository: 44 | 45 | ``` 46 | git clone https://github.com/aws-samples/amazon-chime-sdk-classroom-demo.git 47 | cd amazon-chime-sdk-classroom-demo 48 | npm i 49 | ``` 50 | 51 | Now in the same Terminal pane, run the following command to deploy, package, and create a distribution for your application. Note this will take about 15 minutes. 52 | 53 | ```bash 54 | script/deploy.js -r -a -s -b 55 | ``` 56 | 57 | At the end of the script you will see a URL to a download page. Save this link. 58 | 59 | ### Option 2: Deploy from your local machine 60 | 61 | > Note: Deployment from MacOS has been verified. Local deployment from Windows might require some additional setup. Please refer to the additional resources. 62 | 63 | #### Prerequisites 64 | 65 | To deploy the classroom demo you will need: 66 | 67 | - Node 14 or higher 68 | - npm 6.11 or higher 69 | 70 | And install `aws` and `sam` command line tools: 71 | 72 | - [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv1.html) 73 | - [Install the AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 74 | 75 | First deploy the stack: 76 | 77 | ```bash 78 | git clone https://github.com/aws-samples/amazon-chime-sdk-classroom-demo.git 79 | cd amazon-chime-sdk-classroom-demo 80 | npm i 81 | script/deploy.js -r -a -s -b 82 | ``` 83 | 84 | At the end of the script you will see a URL to a download page. Save this link. To run the application locally in Electron run: 85 | 86 | ```bash 87 | yarn dev 88 | ``` 89 | 90 | Before pushing your commit, ensure that the application works okay in production mode. 91 | 92 | ```bash 93 | yarn cross-env DEBUG_PROD=true yarn start 94 | ``` 95 | 96 | ## Additional Resources 97 | 98 | - Amazon Chime SDK Classroom demo uses electron builder to ship the application for Windows and MacOS. Refer to multi platform build documentation as it is a great resource to help debug packaging failures: [Electron Multi Platform Build](https://www.electron.build/multi-platform-build) 99 | 100 | ## Troubleshooting 101 | 102 | ### I get "The application ... can't be opened" when opening the app 103 | 104 | The default zipping tool on MacOS Catalina may incorrectly unzip the package. Download an alternative (such as "The Unarchiver"), and unzip the package by right clicking and selecting "Open as". You may also need to adjust your security & privacy settings if you get an "unidentified developer" message. 105 | 106 | ### resize2fs: Bad magic number in super-block while trying to open /dev/xvda1 107 | 108 | You might get this error from the `cloud9-resize.sh` script if you are using Cloud9 to deploy. This error is thrown by the deploy script but the volume size is successfully updated. A consecutive run of the deploy script should pass successfully. 109 | -------------------------------------------------------------------------------- /app/.testcafe-electron-rc: -------------------------------------------------------------------------------- 1 | { 2 | "mainWindowUrl": "./app.html", 3 | "appPath": "." 4 | } 5 | -------------------------------------------------------------------------------- /app/Routes.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React, { ReactNode, useContext } from 'react'; 5 | import { Redirect, Route, Switch } from 'react-router-dom'; 6 | 7 | import App from './components/App'; 8 | import Classroom from './components/Classroom'; 9 | import CreateOrJoin from './components/CreateOrJoin'; 10 | import Home from './components/Home'; 11 | import Login from './components/Login'; 12 | import routes from './constants/routes.json'; 13 | import getUIStateContext from './context/getUIStateContext'; 14 | import MeetingStatusProvider from './providers/MeetingStatusProvider'; 15 | 16 | export default function Routes() { 17 | const [state] = useContext(getUIStateContext()); 18 | 19 | const PrivateRoute = ({ 20 | children, 21 | path 22 | }: { 23 | children: ReactNode; 24 | path: string; 25 | }) => { 26 | return ( 27 | 28 | {state.classMode ? children : } 29 | 30 | ); 31 | }; 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/app.global.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | @import '../node_modules/@fortawesome/fontawesome-free/css/all.css'; 7 | @import '../node_modules/rc-tooltip/assets/bootstrap.css'; 8 | @import '../node_modules/react-dropdown/style.css'; 9 | 10 | :root { 11 | --local_video_container_height: 150px; 12 | --right_panel_width: 320px; 13 | --screen_picker_width: 800px; 14 | --screen_picker_height: 600px; 15 | --color_alabaster: #fafafa; 16 | --color_mercury: #e9e9e9; 17 | --color_alto: #dedede; 18 | --color_silver_chalice: #9e9e9e; 19 | --color_tundora: #454545; 20 | --color_mine_shaft_dark: #333; 21 | --color_mine_shaft_medium: #2d2d2d; 22 | --color_mine_shaft_light: #252525; 23 | --color_cod_gray_medium: #111; 24 | --color_cod_gray_light: #1b1b1b; 25 | --color_black: #000; 26 | --color_thunderbird: #d62f12; 27 | --color_black_medium_opacity: rgba(0, 0, 0, 0.6); 28 | --color_black_low_opacity: rgba(0, 0, 0, 0.25); 29 | --color_green: #09ff00; 30 | } 31 | 32 | *, 33 | *::before, 34 | *::after { 35 | box-sizing: border-box; 36 | } 37 | 38 | body { 39 | position: relative; 40 | color: var(--color_alabaster); 41 | background-color: var(--color_mine_shaft_light); 42 | height: 100vh; 43 | margin: 0; 44 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 45 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 46 | sans-serif; 47 | -webkit-font-smoothing: antialiased; 48 | -moz-osx-font-smoothing: grayscale; 49 | overflow: hidden; 50 | line-height: 1.45; 51 | } 52 | 53 | #root { 54 | height: 100%; 55 | animation: fadeIn 1s; 56 | } 57 | 58 | @keyframes fadeIn { 59 | 0% { 60 | opacity: 0; 61 | } 62 | 50% { 63 | opacity: 0; 64 | } 65 | 100% { 66 | opacity: 1; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MyClassroom 6 | 20 | 21 | 22 |
23 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/app/app.icns -------------------------------------------------------------------------------- /app/components/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React, { ReactNode } from 'react'; 5 | import CPUUsage from './CPUUsage'; 6 | 7 | type Props = { 8 | children: ReactNode; 9 | }; 10 | 11 | export default function App(props: Props) { 12 | const { children } = props; 13 | return ( 14 | <> 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/components/CPUUsage.css: -------------------------------------------------------------------------------- 1 | .CPUUsage { 2 | position: fixed; 3 | bottom: 1rem; 4 | left: 1rem; 5 | } 6 | -------------------------------------------------------------------------------- /app/components/CPUUsage.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React, { useEffect, useState } from 'react'; 6 | import { ipcRenderer } from 'electron'; 7 | import { useIntl } from 'react-intl'; 8 | 9 | import styles from './CPUUsage.css'; 10 | 11 | const cx = classNames.bind(styles); 12 | 13 | export default function CPUUsage() { 14 | const intl = useIntl(); 15 | const [visible, setVisible] = useState(false); 16 | const [cpu, setCpu] = useState(null); 17 | 18 | useEffect(() => { 19 | const getCpu = (): number | null => { 20 | try { 21 | return ( 22 | Math.ceil( 23 | ((process.getCPUUsage().percentCPUUsage * 100) / 24 | navigator.hardwareConcurrency) * 25 | 100 26 | ) / 100 27 | ); 28 | } catch (error) { 29 | return null; 30 | } 31 | }; 32 | 33 | let intervalId: number; 34 | ipcRenderer.on('chime-toggle-cpu-usage', (_, argument) => { 35 | clearInterval(intervalId); 36 | if (argument) { 37 | setVisible(true); 38 | setCpu(getCpu()); 39 | intervalId = window.setInterval(() => { 40 | setCpu(getCpu()); 41 | }, 2000); 42 | } else { 43 | setVisible(false); 44 | setCpu(null); 45 | } 46 | }); 47 | 48 | return () => { 49 | clearInterval(intervalId); 50 | }; 51 | }, []); 52 | 53 | let text: string | number = ''; 54 | if (cpu === null) { 55 | text = ''; 56 | } else if (cpu === 0) { 57 | text = intl.formatMessage({ id: 'CPUUsage.getting' }); 58 | } else if (cpu > 0) { 59 | text = `${cpu}%`; 60 | } 61 | 62 | return visible ?
{text}
: <>; 63 | } 64 | -------------------------------------------------------------------------------- /app/components/Chat.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .chat { 7 | display: flex; 8 | flex-direction: column; 9 | width: 100%; 10 | height: 100%; 11 | border-top: 1px solid var(--color_mine_shaft_light); 12 | } 13 | 14 | .messages { 15 | flex: 1 1 auto; 16 | overflow-y: auto; 17 | padding-top: 0.5rem; 18 | } 19 | 20 | .messageWrapper { 21 | padding: 0.5rem 1.25rem; 22 | } 23 | 24 | .senderName { 25 | display: inline; 26 | font-weight: 700; 27 | word-wrap: break-word; 28 | overflow-wrap: break-word; 29 | word-break: break-word; 30 | } 31 | 32 | .date { 33 | display: inline; 34 | margin-left: 0.5rem; 35 | font-size: 0.8rem; 36 | color: var(--color_silver_chalice); 37 | } 38 | 39 | .message { 40 | color: var(--color_alto); 41 | word-wrap: break-word; 42 | overflow-wrap: break-word; 43 | word-break: break-word; 44 | } 45 | 46 | .messageWrapper.raiseHand .message { 47 | font-size: 2rem; 48 | } 49 | 50 | .chatInput { 51 | flex: 0 0 auto; 52 | } 53 | -------------------------------------------------------------------------------- /app/components/Chat.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import moment from 'moment'; 6 | import React, { useContext, useEffect, useRef, useState } from 'react'; 7 | import { DataMessage } from 'amazon-chime-sdk-js'; 8 | 9 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 10 | import getChimeContext from '../context/getChimeContext'; 11 | import styles from './Chat.css'; 12 | import ChatInput from './ChatInput'; 13 | import MessageTopic from '../enums/MessageTopic'; 14 | 15 | const cx = classNames.bind(styles); 16 | 17 | export default function Chat() { 18 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 19 | const [messages, setMessages] = useState([]); 20 | const bottomElement = useRef(null); 21 | 22 | useEffect(() => { 23 | const realTimeMessages: DataMessage[] = []; 24 | const callback = (message: DataMessage) => { 25 | realTimeMessages.push(message); 26 | setMessages(realTimeMessages.slice() as DataMessage[]); 27 | }; 28 | 29 | const chatMessageUpdateCallback = { topic: MessageTopic.Chat, callback }; 30 | const raiseHandMessageUpdateCallback = { 31 | topic: MessageTopic.RaiseHand, 32 | callback 33 | }; 34 | 35 | chime?.subscribeToMessageUpdate(chatMessageUpdateCallback); 36 | chime?.subscribeToMessageUpdate(raiseHandMessageUpdateCallback); 37 | return () => { 38 | chime?.unsubscribeFromMessageUpdate(chatMessageUpdateCallback); 39 | chime?.unsubscribeFromMessageUpdate(raiseHandMessageUpdateCallback); 40 | }; 41 | }, []); 42 | 43 | useEffect(() => { 44 | setTimeout(() => { 45 | ((bottomElement.current as unknown) as HTMLDivElement).scrollIntoView({ 46 | behavior: 'smooth' 47 | }); 48 | }, 10); 49 | }, [messages]); 50 | 51 | return ( 52 |
53 |
54 | {messages.map(message => { 55 | let messageString; 56 | if (message.topic === MessageTopic.Chat) { 57 | messageString = message.text(); 58 | } else if (message.topic === MessageTopic.RaiseHand) { 59 | messageString = `✋`; 60 | } 61 | 62 | return ( 63 |
69 |
70 |
71 | {chime?.roster[message.senderAttendeeId].name} 72 |
73 |
74 | {moment(message.timestampMs).format('h:mm A')} 75 |
76 |
77 |
{messageString}
78 |
79 | ); 80 | })} 81 |
82 |
83 |
84 | 85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /app/components/ChatInput.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .chatInput { 7 | width: 100%; 8 | height: 100%; 9 | padding: 0.75rem; 10 | } 11 | 12 | .form { 13 | display: flex; 14 | background-color: var(--color_tundora); 15 | border-radius: 0.25rem; 16 | } 17 | 18 | .input { 19 | flex: 1 1 auto; 20 | color: var(--color_alabaster); 21 | background-color: transparent; 22 | outline: none; 23 | border: none; 24 | font-size: 1rem; 25 | padding: 0.75rem 0.5rem; 26 | } 27 | 28 | .input::placeholder { 29 | color: var(--color_silver_chalice); 30 | } 31 | 32 | .raiseHandButton { 33 | flex: 0 0 auto; 34 | font-size: 1.5rem; 35 | outline: none; 36 | background-color: transparent; 37 | border: none; 38 | cursor: pointer; 39 | display: flex; 40 | margin-top: 0.25rem; 41 | } 42 | 43 | .raiseHandButton span { 44 | transition: all 0.05s; 45 | filter: grayscale(100%); 46 | } 47 | 48 | .raiseHandButton span:hover { 49 | filter: grayscale(0); 50 | } 51 | 52 | .raiseHandButton.raised span { 53 | filter: grayscale(0) !important; 54 | } 55 | -------------------------------------------------------------------------------- /app/components/ChatInput.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React, { useContext, useEffect, useState } from 'react'; 6 | import { useIntl } from 'react-intl'; 7 | 8 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 9 | import getChimeContext from '../context/getChimeContext'; 10 | import getUIStateContext from '../context/getUIStateContext'; 11 | import ClassMode from '../enums/ClassMode'; 12 | import useFocusMode from '../hooks/useFocusMode'; 13 | import styles from './ChatInput.css'; 14 | import MessageTopic from '../enums/MessageTopic'; 15 | 16 | const cx = classNames.bind(styles); 17 | 18 | let timeoutId: number; 19 | 20 | export default React.memo(function ChatInput() { 21 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 22 | const [state] = useContext(getUIStateContext()); 23 | const [inputText, setInputText] = useState(''); 24 | const [raised, setRaised] = useState(false); 25 | const focusMode = useFocusMode(); 26 | const intl = useIntl(); 27 | 28 | useEffect(() => { 29 | const attendeeId = chime?.configuration?.credentials?.attendeeId; 30 | if (!attendeeId) { 31 | return; 32 | } 33 | 34 | chime?.sendMessage( 35 | raised ? MessageTopic.RaiseHand : MessageTopic.DismissHand, 36 | attendeeId 37 | ); 38 | 39 | if (raised) { 40 | timeoutId = window.setTimeout(() => { 41 | chime?.sendMessage(MessageTopic.DismissHand, attendeeId); 42 | setRaised(false); 43 | }, 10000); 44 | } else { 45 | clearTimeout(timeoutId); 46 | } 47 | }, [raised, chime?.configuration]); 48 | 49 | return ( 50 |
51 |
{ 53 | event.preventDefault(); 54 | }} 55 | className={cx('form')} 56 | > 57 | { 61 | setInputText(event.target.value); 62 | }} 63 | onKeyUp={event => { 64 | event.preventDefault(); 65 | if (focusMode && state.classMode === ClassMode.Student) { 66 | return; 67 | } 68 | if (event.keyCode === 13) { 69 | const sendingMessage = inputText.trim(); 70 | const attendeeId = chime?.configuration?.credentials?.attendeeId; 71 | if (sendingMessage !== '' && attendeeId) { 72 | chime?.sendMessage(MessageTopic.Chat, sendingMessage); 73 | setInputText(''); 74 | } 75 | } 76 | }} 77 | placeholder={intl.formatMessage({ id: 'ChatInput.inputPlaceholder' })} 78 | /> 79 | {state.classMode === ClassMode.Student && ( 80 | 98 | )} 99 |
100 |
101 | ); 102 | }); 103 | -------------------------------------------------------------------------------- /app/components/Classroom.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .classroom { 7 | display: flex; 8 | background: var(--color_mine_shaft_light); 9 | height: 100%; 10 | align-items: center; 11 | justify-content: center; 12 | } 13 | 14 | .classroom.isModeTransitioning::after { 15 | content: ''; 16 | position: fixed; 17 | top: 0; 18 | right: 0; 19 | bottom: 0; 20 | left: 0; 21 | background: var(--color_mine_shaft_light); 22 | z-index: 10; 23 | } 24 | 25 | .left { 26 | flex: 1 1 auto; 27 | display: flex; 28 | flex-direction: column; 29 | height: 100%; 30 | } 31 | 32 | .contentVideoWrapper { 33 | display: none; 34 | flex: 1 1 auto; 35 | overflow-y: hidden; 36 | } 37 | 38 | .classroom.isContentShareEnabled .contentVideoWrapper { 39 | display: block; 40 | } 41 | 42 | .classroom.screenShareMode .contentVideoWrapper { 43 | display: none !important; 44 | } 45 | 46 | .remoteVideoGroupWrapper { 47 | flex: 1 1 auto; 48 | overflow: hidden; 49 | } 50 | 51 | .classroom.roomMode.isContentShareEnabled .remoteVideoGroupWrapper { 52 | flex: 0 0 auto; 53 | } 54 | 55 | .localVideoWrapper { 56 | display: flex; 57 | position: relative; 58 | align-items: center; 59 | justify-content: center; 60 | flex: 0 0 var(--local_video_container_height); 61 | } 62 | 63 | .localVideo { 64 | position: absolute; 65 | right: 0.25rem; 66 | } 67 | 68 | .classroom.screenShareMode .localVideo { 69 | right: auto; 70 | width: 100%; 71 | height: 100%; 72 | padding: 0.25rem; 73 | } 74 | 75 | .classroom.screenShareMode .controls { 76 | z-index: 1; 77 | } 78 | 79 | .right { 80 | display: flex; 81 | flex-direction: column; 82 | flex: 0 0 var(--right_panel_width); 83 | background: var(--color_mine_shaft_medium); 84 | height: 100%; 85 | overflow: hidden; 86 | } 87 | 88 | .classroom.screenShareMode .right { 89 | display: none; 90 | } 91 | 92 | .titleWrapper { 93 | padding: 0.5rem 1rem; 94 | border-bottom: 1px solid var(--color_mine_shaft_light); 95 | } 96 | 97 | .title { 98 | word-wrap: break-word; 99 | overflow-wrap: break-word; 100 | word-break: break-word; 101 | } 102 | 103 | .label { 104 | font-size: 0.8rem; 105 | color: var(--color_silver_chalice); 106 | } 107 | 108 | .deviceSwitcher { 109 | flex: 0 1 auto; 110 | } 111 | 112 | .roster { 113 | flex: 1 1 auto; 114 | overflow-y: scroll; 115 | height: 50%; 116 | } 117 | 118 | .chat { 119 | flex: 1 1 auto; 120 | overflow-y: scroll; 121 | display: flex; 122 | justify-content: center; 123 | align-items: center; 124 | height: 50%; 125 | } 126 | 127 | .modal { 128 | outline: none; 129 | } 130 | 131 | .modalOverlay { 132 | display: flex; 133 | align-items: center; 134 | justify-content: center; 135 | height: 100%; 136 | position: fixed; 137 | top: 0; 138 | right: 0; 139 | bottom: 0; 140 | left: 0; 141 | z-index: 1; 142 | background: rgba(0, 0, 0, 0.5); 143 | backdrop-filter: blur(0.5rem); 144 | } 145 | -------------------------------------------------------------------------------- /app/components/Classroom.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import { ipcRenderer, remote } from 'electron'; 6 | import React, { useCallback, useContext, useEffect, useState } from 'react'; 7 | import { FormattedMessage } from 'react-intl'; 8 | import Modal from 'react-modal'; 9 | 10 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 11 | import getChimeContext from '../context/getChimeContext'; 12 | import getMeetingStatusContext from '../context/getMeetingStatusContext'; 13 | import getUIStateContext from '../context/getUIStateContext'; 14 | import ClassMode from '../enums/ClassMode'; 15 | import MeetingStatus from '../enums/MeetingStatus'; 16 | import ViewMode from '../enums/ViewMode'; 17 | import Chat from './Chat'; 18 | import styles from './Classroom.css'; 19 | import ContentVideo from './ContentVideo'; 20 | import Controls from './Controls'; 21 | import DeviceSwitcher from './DeviceSwitcher'; 22 | import Error from './Error'; 23 | import LoadingSpinner from './LoadingSpinner'; 24 | import LocalVideo from './LocalVideo'; 25 | import RemoteVideoGroup from './RemoteVideoGroup'; 26 | import Roster from './Roster'; 27 | import ScreenPicker from './ScreenPicker'; 28 | import ScreenShareHeader from './ScreenShareHeader'; 29 | 30 | const cx = classNames.bind(styles); 31 | 32 | export default function Classroom() { 33 | Modal.setAppElement('body'); 34 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 35 | const [state] = useContext(getUIStateContext()); 36 | const { meetingStatus, errorMessage } = useContext(getMeetingStatusContext()); 37 | const [isContentShareEnabled, setIsContentShareEnabled] = useState(false); 38 | const [viewMode, setViewMode] = useState(ViewMode.Room); 39 | const [isModeTransitioning, setIsModeTransitioning] = useState(false); 40 | const [isPickerEnabled, setIsPickerEnabled] = useState(false); 41 | 42 | const stopContentShare = async () => { 43 | setIsModeTransitioning(true); 44 | await new Promise(resolve => setTimeout(resolve, 200)); 45 | ipcRenderer.on('chime-disable-screen-share-mode-ack', () => { 46 | try { 47 | chime?.audioVideo?.stopContentShare(); 48 | } catch (error) { 49 | // eslint-disable-next-line 50 | console.error(error); 51 | } finally { 52 | setViewMode(ViewMode.Room); 53 | setIsModeTransitioning(false); 54 | } 55 | }); 56 | ipcRenderer.send('chime-disable-screen-share-mode'); 57 | }; 58 | 59 | // Must pass a memoized callback to the ContentVideo component using useCallback(). 60 | // ContentVideo will re-render only when one dependency "viewMode" changes. 61 | // See more comments in ContentVideo. 62 | const onContentShareEnabled = useCallback( 63 | async (enabled: boolean) => { 64 | if (enabled && viewMode === ViewMode.ScreenShare) { 65 | await stopContentShare(); 66 | } 67 | setIsContentShareEnabled(enabled); 68 | }, 69 | [viewMode] 70 | ); 71 | 72 | if (process.env.NODE_ENV === 'production') { 73 | useEffect(() => { 74 | // Recommend using "onbeforeunload" over "addEventListener" 75 | window.onbeforeunload = async (event: BeforeUnloadEvent) => { 76 | // Prevent the window from closing immediately 77 | // eslint-disable-next-line 78 | event.returnValue = true; 79 | try { 80 | await chime?.leaveRoom(state.classMode === ClassMode.Teacher); 81 | } catch (error) { 82 | // eslint-disable-next-line 83 | console.error(error); 84 | } finally { 85 | window.onbeforeunload = null; 86 | remote.app.quit(); 87 | } 88 | }; 89 | return () => { 90 | window.onbeforeunload = null; 91 | }; 92 | }, []); 93 | } 94 | 95 | return ( 96 |
104 | {meetingStatus === MeetingStatus.Loading && } 105 | {meetingStatus === MeetingStatus.Failed && ( 106 | 107 | )} 108 | {meetingStatus === MeetingStatus.Succeeded && ( 109 | <> 110 | <> 111 |
112 | {viewMode === ViewMode.ScreenShare && ( 113 | 114 | )} 115 |
116 | 117 |
118 |
119 | 123 |
124 |
125 |
126 | { 129 | setIsPickerEnabled(true); 130 | }} 131 | /> 132 |
133 |
134 | 135 |
136 |
137 |
138 |
139 |
140 |
{chime?.title}
141 |
142 | 143 |
144 |
145 |
146 | 147 |
148 |
149 | 150 |
151 |
152 | 153 |
154 |
155 | 156 | { 162 | setIsPickerEnabled(false); 163 | }} 164 | > 165 | { 167 | setIsModeTransitioning(true); 168 | await new Promise(resolve => setTimeout(resolve, 200)); 169 | ipcRenderer.on( 170 | 'chime-enable-screen-share-mode-ack', 171 | async () => { 172 | try { 173 | setIsPickerEnabled(false); 174 | await chime?.audioVideo?.startContentShareFromScreenCapture( 175 | selectedSourceId 176 | ); 177 | setViewMode(ViewMode.ScreenShare); 178 | setIsModeTransitioning(false); 179 | } catch (error) { 180 | // eslint-disable-next-line 181 | console.error(error); 182 | await stopContentShare(); 183 | } 184 | } 185 | ); 186 | ipcRenderer.send('chime-enable-screen-share-mode'); 187 | }} 188 | onClickCancelButton={() => { 189 | setIsPickerEnabled(false); 190 | }} 191 | /> 192 | 193 | 194 | )} 195 |
196 | ); 197 | } 198 | -------------------------------------------------------------------------------- /app/components/ContentVideo.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .contentVideo { 7 | display: block; 8 | background-color: var(--color_black); 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | .video { 14 | display: block; 15 | width: 100%; 16 | height: 100%; 17 | object-fit: contain; 18 | } 19 | -------------------------------------------------------------------------------- /app/components/ContentVideo.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { DefaultModality, VideoTileState } from 'amazon-chime-sdk-js'; 5 | import classNames from 'classnames/bind'; 6 | import React, { useContext, useEffect, useRef, useState } from 'react'; 7 | 8 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 9 | import getChimeContext from '../context/getChimeContext'; 10 | import styles from './ContentVideo.css'; 11 | 12 | const cx = classNames.bind(styles); 13 | 14 | type Props = { 15 | onContentShareEnabled: (enabled: boolean) => void; 16 | }; 17 | 18 | export default function ContentVideo(props: Props) { 19 | const { onContentShareEnabled } = props; 20 | const [enabled, setEnabled] = useState(false); 21 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 22 | const videoElement = useRef(null); 23 | 24 | // Note that this useEffect takes no dependency (an empty array [] as a second argument). 25 | // Thus, calling props.onContentShareEnabled in the passed functon will point to an old prop 26 | // even if a parent component passes a new prop. See comments for the second useEffect. 27 | useEffect(() => { 28 | const contentTileIds = new Set(); 29 | chime?.audioVideo?.addObserver({ 30 | videoTileDidUpdate: (tileState: VideoTileState): void => { 31 | if ( 32 | !tileState.boundAttendeeId || 33 | !tileState.isContent || 34 | !tileState.tileId 35 | ) { 36 | return; 37 | } 38 | 39 | const modality = new DefaultModality(tileState.boundAttendeeId); 40 | if ( 41 | modality.base() === 42 | chime?.meetingSession?.configuration.credentials?.attendeeId && 43 | modality.hasModality(DefaultModality.MODALITY_CONTENT) 44 | ) { 45 | return; 46 | } 47 | 48 | chime?.audioVideo?.bindVideoElement( 49 | tileState.tileId, 50 | (videoElement.current as unknown) as HTMLVideoElement 51 | ); 52 | 53 | if (tileState.active) { 54 | contentTileIds.add(tileState.tileId); 55 | setEnabled(true); 56 | } else { 57 | contentTileIds.delete(tileState.tileId); 58 | setEnabled(contentTileIds.size > 0); 59 | } 60 | }, 61 | videoTileWasRemoved: (tileId: number): void => { 62 | if (contentTileIds.has(tileId)) { 63 | contentTileIds.delete(tileId); 64 | setEnabled(contentTileIds.size > 0); 65 | } 66 | } 67 | }); 68 | }, []); 69 | 70 | // Call props.onContentShareEnabled in this useEffect. Also, this useEffect does not depend on 71 | // props.onContentShareEnabled to avoid an unnecessary execution. Whenever the function 72 | // is invoked per enabled change, it will reference the latest onContentShareEnabled. 73 | useEffect(() => { 74 | onContentShareEnabled(enabled); 75 | }, [enabled]); 76 | 77 | return ( 78 |
79 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /app/components/Controls.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .controls { 7 | display: block; 8 | position: relative; 9 | } 10 | 11 | .controls.screenShareMode { 12 | width: calc(var(--local_video_container_height) - 0.5rem); 13 | height: calc(var(--local_video_container_height) - 0.5rem); 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | transition: opacity 0.15s; 18 | } 19 | 20 | .controls.screenShareMode:hover { 21 | opacity: 1 !important; 22 | } 23 | 24 | .controls.screenShareMode.videoEnabled { 25 | opacity: 0; 26 | } 27 | 28 | .controls.screenShareMode.audioMuted { 29 | opacity: 1; 30 | } 31 | 32 | .micMuted { 33 | display: none; 34 | z-index: 0; 35 | position: absolute; 36 | top: 0; 37 | left: 0; 38 | width: 100%; 39 | height: 100%; 40 | background-color: var(--color_black_medium_opacity); 41 | text-align: center; 42 | justify-content: center; 43 | border-radius: 0.25rem; 44 | font-size: 1rem; 45 | padding: 1rem; 46 | } 47 | 48 | .controls.screenShareMode.audioMuted.videoEnabled .micMuted { 49 | display: flex; 50 | } 51 | 52 | .controls.screenShareMode.audioMuted.videoEnabled .muteButton { 53 | background-color: var(--color_thunderbird); 54 | } 55 | 56 | .controls button { 57 | width: 2.75rem; 58 | height: 2.75rem; 59 | text-align: center; 60 | border: none; 61 | border-radius: 50%; 62 | font-size: 1.25rem; 63 | color: var(--color_alabaster); 64 | background: var(--color_tundora); 65 | cursor: pointer; 66 | transition: opacity 0.15s; 67 | outline: none; 68 | z-index: 1; 69 | } 70 | 71 | .controls button.enabled { 72 | color: var(--color_tundora); 73 | background: var(--color_alabaster); 74 | } 75 | 76 | .controls button:hover { 77 | opacity: 0.8; 78 | } 79 | 80 | .controls.roomMode button + button { 81 | margin-left: 1rem; 82 | } 83 | 84 | .controls.screenShareMode button + button { 85 | margin-left: 0.5rem; 86 | } 87 | 88 | .endButton { 89 | background: var(--color_thunderbird) !important; 90 | } 91 | -------------------------------------------------------------------------------- /app/components/Controls.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React, { useContext, useEffect, useState } from 'react'; 6 | import { FormattedMessage, useIntl } from 'react-intl'; 7 | import { useHistory } from 'react-router-dom'; 8 | 9 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 10 | import routes from '../constants/routes.json'; 11 | import getChimeContext from '../context/getChimeContext'; 12 | import getUIStateContext from '../context/getUIStateContext'; 13 | import ClassMode from '../enums/ClassMode'; 14 | import ViewMode from '../enums/ViewMode'; 15 | import styles from './Controls.css'; 16 | import Tooltip from './Tooltip'; 17 | import MessageTopic from '../enums/MessageTopic'; 18 | 19 | const cx = classNames.bind(styles); 20 | 21 | enum VideoStatus { 22 | Disabled, 23 | Loading, 24 | Enabled 25 | } 26 | 27 | type Props = { 28 | viewMode: ViewMode; 29 | onClickShareButton: () => void; 30 | }; 31 | 32 | export default function Controls(props: Props) { 33 | const { viewMode, onClickShareButton } = props; 34 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 35 | const [state] = useContext(getUIStateContext()); 36 | const history = useHistory(); 37 | const [muted, setMuted] = useState(false); 38 | const [focus, setFocus] = useState(false); 39 | const [videoStatus, setVideoStatus] = useState(VideoStatus.Disabled); 40 | const intl = useIntl(); 41 | 42 | useEffect(() => { 43 | const callback = (localMuted: boolean) => { 44 | setMuted(localMuted); 45 | }; 46 | chime?.audioVideo?.realtimeSubscribeToMuteAndUnmuteLocalAudio(callback); 47 | return () => { 48 | if (chime && chime?.audioVideo) { 49 | chime?.audioVideo?.realtimeUnsubscribeToMuteAndUnmuteLocalAudio( 50 | callback 51 | ); 52 | } 53 | }; 54 | }, []); 55 | 56 | return ( 57 |
65 |
66 | 67 |
68 | {state.classMode === ClassMode.Teacher && viewMode === ViewMode.Room && ( 69 | 76 | 99 | 100 | )} 101 | 108 | 129 | 130 | 137 | 174 | 175 | {state.classMode === ClassMode.Teacher && 176 | viewMode !== ViewMode.ScreenShare && ( 177 | 180 | 189 | 190 | )} 191 | {viewMode !== ViewMode.ScreenShare && ( 192 | 199 | 209 | 210 | )} 211 |
212 | ); 213 | } 214 | -------------------------------------------------------------------------------- /app/components/CreateOrJoin.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .createOrJoin { 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | height: 100%; 11 | color: var(--color_mine_shaft_dark); 12 | background: var(--color_mine_shaft_light); 13 | } 14 | 15 | .title { 16 | font-size: 1.5rem; 17 | font-weight: 500; 18 | text-align: center; 19 | margin: 0 0 1rem; 20 | } 21 | 22 | .formWrapper { 23 | width: 400px; 24 | background: var(--color_alabaster); 25 | margin: auto; 26 | border: none; 27 | border-radius: 0.25rem; 28 | padding: 3rem 2rem; 29 | } 30 | 31 | .form { 32 | width: 100%; 33 | } 34 | 35 | .titleInput, 36 | .nameInput { 37 | display: block; 38 | width: 100%; 39 | margin-bottom: 0.75rem; 40 | padding: 0.75rem 1rem; 41 | font-size: 1rem; 42 | border-radius: 0.25rem; 43 | border: 1px solid var(--color_alto); 44 | background-color: transparent; 45 | } 46 | 47 | .button { 48 | width: 100%; 49 | border: none; 50 | border-radius: 0.25rem; 51 | padding: 0.75rem; 52 | font-size: 1.1rem; 53 | font-weight: 500; 54 | color: var(--color_alabaster); 55 | background: var(--color_mine_shaft_light); 56 | cursor: pointer; 57 | transition: background-color 0.15s; 58 | user-select: none; 59 | } 60 | 61 | .button:hover { 62 | background: var(--color_cod_gray_medium); 63 | } 64 | 65 | .loginLink { 66 | display: inline-block; 67 | color: var(--color_tundora); 68 | background: var(--color_alabaster); 69 | cursor: pointer; 70 | transition: opacity 0.15s; 71 | user-select: none; 72 | text-decoration: none; 73 | margin-top: 0.5rem; 74 | } 75 | 76 | .loginLink:hover { 77 | text-decoration: underline; 78 | } 79 | 80 | .regionsList { 81 | margin-bottom: 0.75rem; 82 | border-radius: 0.25rem; 83 | border: 1px solid var(--color_alto); 84 | } 85 | 86 | .control { 87 | background-color: transparent; 88 | cursor: pointer; 89 | border: none !important; 90 | outline: none !important; 91 | box-shadow: none !important; 92 | transition: none; 93 | border-radius: 0.25rem; 94 | padding: 0 1rem; 95 | height: 2.75rem; 96 | color: var(--color_mine_shaft_light); 97 | display: flex; 98 | align-items: center; 99 | } 100 | 101 | .arrow { 102 | border-color: var(--color_tundora) transparent transparent; 103 | border-width: 0.3rem 0.3rem 0; 104 | margin-top: 0.25rem; 105 | margin-right: 0.25rem; 106 | } 107 | 108 | .dropdown[class~='is-open'] .arrow { 109 | border-color: var(--color_tundora) transparent transparent !important; 110 | border-width: 0.3rem 0.3rem 0 !important; 111 | } 112 | 113 | .menu { 114 | margin: 0; 115 | padding: 0.5rem; 116 | color: var(--color_tundora); 117 | background-color: var(--color_alabaster); 118 | box-shadow: 0 0 0 1px var(--color_alto), 119 | 0 0.5rem 1rem var(--color_black_low_opacity); 120 | overflow: hidden; 121 | border: none; 122 | max-height: 16.5rem; 123 | border-radius: 0.25rem; 124 | overflow-y: scroll; 125 | } 126 | 127 | .menu *[class~='Dropdown-option'] { 128 | color: var(--color_tundora); 129 | border-radius: 0.25rem; 130 | padding: 0.5rem; 131 | } 132 | 133 | .menu *[class~='Dropdown-option']:hover { 134 | background-color: var(--color_mercury); 135 | } 136 | 137 | .menu *[class~='is-selected'] { 138 | background-color: transparent; 139 | } 140 | 141 | .menu *[class~='is-selected']:hover { 142 | background-color: var(--color_mercury); 143 | } 144 | -------------------------------------------------------------------------------- /app/components/CreateOrJoin.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React, { useContext, useEffect, useState } from 'react'; 6 | import Dropdown, { Option } from 'react-dropdown'; 7 | import { FormattedMessage, useIntl } from 'react-intl'; 8 | import { Link, useHistory } from 'react-router-dom'; 9 | 10 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 11 | import routes from '../constants/routes.json'; 12 | import getChimeContext from '../context/getChimeContext'; 13 | import getUIStateContext from '../context/getUIStateContext'; 14 | import ClassMode from '../enums/ClassMode'; 15 | import RegionType from '../types/RegionType'; 16 | import styles from './CreateOrJoin.css'; 17 | import OptionalFeature from '../enums/OptionalFeature'; 18 | 19 | const cx = classNames.bind(styles); 20 | 21 | const optionalFeatures = [ 22 | { label: 'None', value: OptionalFeature.None }, 23 | { label: 'Enable Simulcast For Chrome', value: OptionalFeature.Simulcast } 24 | ]; 25 | 26 | export default function CreateOrJoin() { 27 | const chime = useContext(getChimeContext()) as ChimeSdkWrapper; 28 | const [state] = useContext(getUIStateContext()); 29 | const [title, setTitle] = useState(''); 30 | const [name, setName] = useState(''); 31 | const [region, setRegion] = useState(undefined); 32 | const [optionalFeature, setOptionalFeature] = useState(''); 33 | const history = useHistory(); 34 | const intl = useIntl(); 35 | 36 | useEffect(() => { 37 | setOptionalFeature(optionalFeatures[0].value); 38 | (async () => { 39 | setRegion(await chime?.lookupClosestChimeRegion()); 40 | })(); 41 | }, []); 42 | 43 | return ( 44 |
45 |
46 |

47 | {state.classMode === ClassMode.Teacher ? ( 48 | 49 | ) : ( 50 | 51 | )} 52 |

53 |
{ 56 | event.preventDefault(); 57 | if (title && name && region) { 58 | history.push( 59 | `/classroom?title=${encodeURIComponent( 60 | title 61 | )}&name=${encodeURIComponent(name)}®ion=${ 62 | region.value 63 | }&optionalFeature=${optionalFeature}` 64 | ); 65 | } 66 | }} 67 | > 68 | { 71 | setTitle(event.target.value); 72 | }} 73 | placeholder={intl.formatMessage({ 74 | id: 'CreateOrJoin.titlePlaceholder' 75 | })} 76 | /> 77 | { 80 | setName(event.target.value); 81 | }} 82 | placeholder={intl.formatMessage({ 83 | id: 'CreateOrJoin.namePlaceholder' 84 | })} 85 | /> 86 | {state.classMode === ClassMode.Teacher && ( 87 |
88 | { 100 | setRegion(selectedRegion); 101 | }} 102 | placeholder="" 103 | /> 104 |
105 | )} 106 | 107 |
108 | { 117 | setOptionalFeature(selectedFeature.value); 118 | }} 119 | placeholder={optionalFeatures[0].label} 120 | /> 121 |
122 | 123 | 126 |
127 | 128 | {state.classMode === ClassMode.Teacher ? ( 129 | 130 | ) : ( 131 | 132 | )} 133 | 134 |
135 |
136 | ); 137 | } 138 | -------------------------------------------------------------------------------- /app/components/DeviceSwitcher.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .deviceList { 7 | padding: 0.5rem; 8 | } 9 | 10 | .control { 11 | background-color: transparent; 12 | cursor: pointer; 13 | border: none !important; 14 | outline: none !important; 15 | box-shadow: none !important; 16 | transition: none; 17 | border-radius: 0.25rem; 18 | font-size: 0.9rem; 19 | padding: 0.5rem; 20 | } 21 | 22 | .control:hover { 23 | background-color: var(--color_mine_shaft_light); 24 | } 25 | 26 | .placeholder { 27 | color: var(--color_alto); 28 | } 29 | 30 | .arrow { 31 | border-color: var(--color_alto) transparent transparent; 32 | border-width: 0.3rem 0.3rem 0; 33 | margin-top: 2px; 34 | margin-right: 0.25rem; 35 | } 36 | 37 | .dropdown[class~='is-open'] .arrow { 38 | border-color: var(--color_alto) transparent transparent !important; 39 | border-width: 0.3rem 0.3rem 0 !important; 40 | } 41 | 42 | .menu { 43 | margin: 0; 44 | padding: 0.5rem; 45 | color: var(--color_alto); 46 | background-color: var(--color_cod_gray_medium); 47 | box-shadow: 0 0.25rem 0.5rem var(--color_black_low_opacity); 48 | overflow: hidden; 49 | font-size: 0.9rem; 50 | border: none; 51 | max-height: none; 52 | border-radius: 0.25rem; 53 | } 54 | 55 | .menu *[class~='Dropdown-option'] { 56 | color: var(--color_silver_chalice); 57 | border-radius: 0.25rem; 58 | } 59 | 60 | .menu *[class~='Dropdown-option']:hover { 61 | background-color: var(--color_cod_gray_light); 62 | } 63 | 64 | .menu *[class~='is-selected'] { 65 | background-color: transparent; 66 | color: var(--color_alabaster); 67 | } 68 | 69 | .menu *[class~='is-selected']:hover { 70 | background-color: var(--color_cod_gray_light); 71 | } 72 | -------------------------------------------------------------------------------- /app/components/DeviceSwitcher.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React, { useContext } from 'react'; 6 | import Dropdown from 'react-dropdown'; 7 | import { useIntl } from 'react-intl'; 8 | 9 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 10 | import getChimeContext from '../context/getChimeContext'; 11 | import useDevices from '../hooks/useDevices'; 12 | import DeviceType from '../types/DeviceType'; 13 | import styles from './DeviceSwitcher.css'; 14 | 15 | const cx = classNames.bind(styles); 16 | 17 | export default function DeviceSwitcher() { 18 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 19 | const intl = useIntl(); 20 | const deviceSwitcherState = useDevices(); 21 | 22 | return ( 23 |
24 | { 37 | await chime?.chooseAudioInputDevice(selectedDevice); 38 | }} 39 | placeholder={ 40 | deviceSwitcherState.currentAudioInputDevice 41 | ? intl.formatMessage({ 42 | id: 'DeviceSwitcher.noAudioInputPlaceholder' 43 | }) 44 | : '' 45 | } 46 | /> 47 | { 60 | await chime?.chooseAudioOutputDevice(selectedDevice); 61 | }} 62 | placeholder={ 63 | deviceSwitcherState.currentAudioOutputDevice 64 | ? intl.formatMessage({ 65 | id: 'DeviceSwitcher.noAudioOutputPlaceholder' 66 | }) 67 | : '' 68 | } 69 | /> 70 | { 83 | await chime?.chooseVideoInputDevice(selectedDevice); 84 | }} 85 | placeholder={ 86 | deviceSwitcherState.currentVideoInputDevice 87 | ? intl.formatMessage({ 88 | id: 'DeviceSwitcher.noVideoInputPlaceholder' 89 | }) 90 | : '' 91 | } 92 | /> 93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /app/components/Error.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .error { 7 | text-align: center; 8 | max-width: 50%; 9 | } 10 | 11 | .errorMessage { 12 | word-wrap: break-word; 13 | overflow-wrap: break-word; 14 | word-break: break-word; 15 | font-size: 1.5rem; 16 | } 17 | 18 | .goHomeLink { 19 | display: inline-block; 20 | border: none; 21 | border-radius: 0.25rem; 22 | padding: 0.75rem 2rem; 23 | font-size: 1.1rem; 24 | font-weight: 500; 25 | color: var(--color_mine_shaft_light); 26 | background: var(--color_alabaster); 27 | cursor: pointer; 28 | transition: opacity 0.15s; 29 | user-select: none; 30 | text-decoration: none; 31 | margin-top: 2.5rem; 32 | } 33 | 34 | .goHomeLink:hover { 35 | opacity: 0.8; 36 | } 37 | -------------------------------------------------------------------------------- /app/components/Error.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React, { ReactNode } from 'react'; 6 | import { Link } from 'react-router-dom'; 7 | 8 | import routes from '../constants/routes.json'; 9 | import styles from './Error.css'; 10 | 11 | const cx = classNames.bind(styles); 12 | 13 | type Props = { 14 | errorMessage: ReactNode; 15 | }; 16 | 17 | export default function Error(props: Props) { 18 | const { errorMessage } = props; 19 | return ( 20 |
21 |
22 | {errorMessage || 'Something went wrong'} 23 |
24 | 25 | Take me home 26 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/components/Home.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .home { 7 | display: block; 8 | height: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /app/components/Home.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React, { useContext } from 'react'; 6 | import { Redirect } from 'react-router-dom'; 7 | 8 | import routes from '../constants/routes.json'; 9 | import getUIStateContext from '../context/getUIStateContext'; 10 | import styles from './Home.css'; 11 | 12 | const cx = classNames.bind(styles); 13 | 14 | export default function Home() { 15 | const [state] = useContext(getUIStateContext()); 16 | return ( 17 |
18 | {state.classMode ? ( 19 | 20 | ) : ( 21 | 22 | )} 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/components/LoadingSpinner.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .loadingSpinner { 7 | display: block; 8 | } 9 | 10 | .spinner { 11 | margin: auto; 12 | width: 40px; 13 | height: 40px; 14 | position: relative; 15 | } 16 | 17 | .circle { 18 | width: 100%; 19 | height: 100%; 20 | position: absolute; 21 | left: 0; 22 | top: 0; 23 | } 24 | 25 | .circle::before { 26 | content: ''; 27 | display: block; 28 | margin: 0 auto; 29 | width: 15%; 30 | height: 15%; 31 | background-color: var(--color_alabaster); 32 | border-radius: 100%; 33 | animation: circleFadeDelay 1.2s infinite ease-in-out both; 34 | } 35 | 36 | .circle2 { 37 | transform: rotate(30deg); 38 | } 39 | 40 | .circle3 { 41 | transform: rotate(60deg); 42 | } 43 | 44 | .circle4 { 45 | transform: rotate(90deg); 46 | } 47 | 48 | .circle5 { 49 | transform: rotate(120deg); 50 | } 51 | 52 | .circle6 { 53 | transform: rotate(150deg); 54 | } 55 | 56 | .circle7 { 57 | transform: rotate(180deg); 58 | } 59 | 60 | .circle8 { 61 | transform: rotate(210deg); 62 | } 63 | 64 | .circle9 { 65 | transform: rotate(240deg); 66 | } 67 | 68 | .circle10 { 69 | transform: rotate(270deg); 70 | } 71 | 72 | .circle11 { 73 | transform: rotate(300deg); 74 | } 75 | 76 | .circle12 { 77 | transform: rotate(330deg); 78 | } 79 | 80 | .circle2::before { 81 | animation-delay: -1.1s; 82 | } 83 | 84 | .circle3::before { 85 | animation-delay: -1s; 86 | } 87 | 88 | .circle4::before { 89 | animation-delay: -0.9s; 90 | } 91 | 92 | .circle5::before { 93 | animation-delay: -0.8s; 94 | } 95 | 96 | .circle6::before { 97 | animation-delay: -0.7s; 98 | } 99 | 100 | .circle7::before { 101 | animation-delay: -0.6s; 102 | } 103 | 104 | .circle8::before { 105 | animation-delay: -0.5s; 106 | } 107 | 108 | .circle9::before { 109 | animation-delay: -0.4s; 110 | } 111 | 112 | .circle10::before { 113 | animation-delay: -0.3s; 114 | } 115 | 116 | .circle11::before { 117 | animation-delay: -0.2s; 118 | } 119 | 120 | .circle12::before { 121 | animation-delay: -0.1s; 122 | } 123 | 124 | @keyframes circleFadeDelay { 125 | 0%, 126 | 39%, 127 | 100% { 128 | opacity: 0; 129 | } 130 | 40% { 131 | opacity: 1; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/components/LoadingSpinner.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React from 'react'; 6 | 7 | import styles from './LoadingSpinner.css'; 8 | 9 | const cx = classNames.bind(styles); 10 | 11 | export default function LoadingSpinner() { 12 | return ( 13 |
14 |
15 | {Array.from(Array(12).keys()).map(key => ( 16 |
17 | ))} 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/components/LocalVideo.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .localVideo { 7 | display: none; 8 | width: calc(var(--local_video_container_height) - 0.5rem); 9 | height: calc(var(--local_video_container_height) - 0.5rem); 10 | background: var(--color_black); 11 | border-radius: 0.25rem; 12 | overflow: hidden; 13 | } 14 | 15 | .localVideo.enabled { 16 | display: block !important; 17 | } 18 | 19 | .video { 20 | display: block; 21 | width: 100%; 22 | height: 100%; 23 | border-radius: 0.25rem; 24 | object-fit: cover; 25 | } 26 | -------------------------------------------------------------------------------- /app/components/LocalVideo.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { VideoTileState } from 'amazon-chime-sdk-js'; 5 | import classNames from 'classnames/bind'; 6 | import React, { useContext, useEffect, useRef, useState } from 'react'; 7 | 8 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 9 | import getChimeContext from '../context/getChimeContext'; 10 | import styles from './LocalVideo.css'; 11 | 12 | const cx = classNames.bind(styles); 13 | 14 | export default function LocalVideo() { 15 | const [enabled, setEnabled] = useState(false); 16 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 17 | const videoElement = useRef(null); 18 | 19 | useEffect(() => { 20 | chime?.audioVideo?.addObserver({ 21 | videoTileDidUpdate: (tileState: VideoTileState): void => { 22 | if ( 23 | !tileState.boundAttendeeId || 24 | !tileState.localTile || 25 | !tileState.tileId || 26 | !videoElement.current 27 | ) { 28 | return; 29 | } 30 | chime?.audioVideo?.bindVideoElement( 31 | tileState.tileId, 32 | (videoElement.current as unknown) as HTMLVideoElement 33 | ); 34 | setEnabled(tileState.active); 35 | } 36 | }); 37 | }, []); 38 | 39 | return ( 40 |
45 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /app/components/Login.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .login { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | height: 100%; 11 | } 12 | 13 | .content { 14 | color: var(--color_mine_shaft_dark); 15 | width: var(--screen_picker_width); 16 | background-color: var(--color_alabaster); 17 | border-radius: 0.25rem; 18 | padding: 5rem 5rem 6rem; 19 | } 20 | 21 | .title { 22 | margin: 0 0 1rem; 23 | } 24 | 25 | .selection { 26 | display: flex; 27 | } 28 | 29 | .selection div { 30 | width: 50%; 31 | } 32 | 33 | .selection div + div { 34 | margin-left: 3rem; 35 | } 36 | 37 | .selection h2 { 38 | font-size: 1.4rem; 39 | } 40 | 41 | .selection ul { 42 | padding-left: 1.5rem; 43 | font-size: 1.1rem; 44 | } 45 | 46 | .selection ul ul { 47 | padding-top: 0.5rem; 48 | } 49 | 50 | .selection li + li { 51 | margin-top: 0.5rem; 52 | } 53 | 54 | .selection button { 55 | border: none; 56 | border-radius: 0.25rem; 57 | padding: 0.75rem 1rem; 58 | font-size: 1.1rem; 59 | font-weight: 500; 60 | color: var(--color_alabaster); 61 | background: var(--color_mine_shaft_light); 62 | cursor: pointer; 63 | transition: background-color 0.15s; 64 | user-select: none; 65 | margin-top: 0.75rem; 66 | } 67 | 68 | .selection button:hover { 69 | background: var(--color_cod_gray_medium); 70 | } 71 | -------------------------------------------------------------------------------- /app/components/Login.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React, { useContext, useEffect } from 'react'; 6 | import { FormattedMessage } from 'react-intl'; 7 | import { useHistory } from 'react-router-dom'; 8 | 9 | import localStorageKeys from '../constants/localStorageKeys.json'; 10 | import routes from '../constants/routes.json'; 11 | import getUIStateContext from '../context/getUIStateContext'; 12 | import ClassMode from '../enums/ClassMode'; 13 | import styles from './Login.css'; 14 | 15 | const cx = classNames.bind(styles); 16 | 17 | export default function Login() { 18 | const [, dispatch] = useContext(getUIStateContext()); 19 | const history = useHistory(); 20 | 21 | useEffect(() => { 22 | localStorage.clear(); 23 | dispatch({ 24 | type: 'SET_CLASS_MODE', 25 | payload: { 26 | classMode: null 27 | } 28 | }); 29 | }, []); 30 | 31 | return ( 32 |
33 |
34 |

35 | 36 |

37 |
38 |
39 |

40 | 41 |

42 |
    43 |
  • 44 | 45 |
  • 46 |
  • 47 | 48 |
  • 49 |
  • 50 | 51 |
  • 52 |
  • 53 | 54 |
  • 55 |
      56 |
    • 57 | 58 |
    • 59 |
    • 60 | 61 |
    • 62 |
    63 |
64 | 82 |
83 |
84 |

85 | 86 |

87 |
    88 |
  • 89 | 90 |
  • 91 |
  • 92 | 93 |
  • 94 |
  • 95 | 96 |
  • 97 |
  • 98 | 99 |
  • 100 |
      101 |
    • 102 | 103 |
    • 104 |
    • 105 | 106 |
    • 107 |
    108 |
109 | 127 |
128 |
129 |
130 |
131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /app/components/RemoteVideo.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .remoteVideo { 7 | display: none; 8 | position: relative; 9 | background: transparent; 10 | overflow: hidden; 11 | } 12 | 13 | .remoteVideo.roomMode { 14 | border-radius: 0.25rem; 15 | } 16 | 17 | .remoteVideo.screenShareMode { 18 | border-radius: 0.25rem; 19 | } 20 | 21 | .remoteVideo.enabled { 22 | display: block !important; 23 | } 24 | 25 | .remoteVideo.activeSpeaker { 26 | border: 3px solid var(--color_green); 27 | } 28 | 29 | .video { 30 | display: block; 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | width: 100%; 35 | height: 100%; 36 | object-fit: cover; 37 | } 38 | 39 | .raisedHand { 40 | z-index: 1; 41 | position: absolute; 42 | top: 0.25rem; 43 | left: 0.25rem; 44 | font-size: 2rem; 45 | animation: shake 1.22s cubic-bezier(0.36, 0.07, 0.19, 0.97) infinite both; 46 | transform: translate3d(0, 0, 0); 47 | backface-visibility: hidden; 48 | perspective: 1000px; 49 | user-select: none; 50 | } 51 | 52 | @keyframes shake { 53 | 10%, 54 | 90% { 55 | transform: translate3d(-0.5px, 0, 0); 56 | } 57 | 58 | 20%, 59 | 80% { 60 | transform: translate3d(1px, 0, 0); 61 | } 62 | 63 | 30%, 64 | 50%, 65 | 70% { 66 | transform: translate3d(-1.5px, 0, 0); 67 | } 68 | 69 | 40%, 70 | 60% { 71 | transform: translate3d(1.5px, 0, 0); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/components/RemoteVideo.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React from 'react'; 6 | import { useIntl } from 'react-intl'; 7 | 8 | import ViewMode from '../enums/ViewMode'; 9 | import Size from '../enums/Size'; 10 | import VideoNameplate from './VideoNameplate'; 11 | import styles from './RemoteVideo.css'; 12 | 13 | const cx = classNames.bind(styles); 14 | 15 | type Props = { 16 | viewMode: ViewMode; 17 | enabled: boolean; 18 | videoElementRef: (instance: HTMLVideoElement | null) => void; 19 | size: Size; 20 | attendeeId: string | null; 21 | raisedHand?: boolean; 22 | activeSpeaker?: boolean; 23 | isContentShareEnabled: boolean; 24 | }; 25 | 26 | export default function RemoteVideo(props: Props) { 27 | const intl = useIntl(); 28 | const { 29 | viewMode, 30 | enabled, 31 | videoElementRef, 32 | size = Size.Large, 33 | attendeeId, 34 | raisedHand, 35 | activeSpeaker, 36 | isContentShareEnabled 37 | } = props; 38 | return ( 39 |
47 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /app/components/RemoteVideoGroup.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .remoteVideoGroup { 7 | display: grid; 8 | position: relative; 9 | height: 100%; 10 | grid-gap: 0.25rem; 11 | } 12 | 13 | .remoteVideoGroup.roomMode { 14 | padding: 0.25rem 0.25rem 0; 15 | } 16 | 17 | .remoteVideoGroup.roomMode.isContentShareEnabled { 18 | height: var(--local_video_container_height); 19 | } 20 | 21 | .remoteVideoGroup.screenShareMode { 22 | padding: 0 0.25rem; 23 | grid-template-columns: repeat(2, 1fr); 24 | grid-template-rows: repeat(8, 1fr); 25 | } 26 | 27 | /* Room mode */ 28 | 29 | .remoteVideoGroup.roomMode.remoteVideoGroup-1 { 30 | grid-template-columns: repeat(1, 1fr); 31 | grid-template-rows: repeat(1, 1fr); 32 | } 33 | 34 | .remoteVideoGroup.roomMode.remoteVideoGroup-2, 35 | .remoteVideoGroup.roomMode.remoteVideoGroup-3, 36 | .remoteVideoGroup.roomMode.remoteVideoGroup-4 { 37 | grid-template-columns: repeat(2, 1fr); 38 | grid-template-rows: repeat(2, 1fr); 39 | } 40 | 41 | .remoteVideoGroup.roomMode.remoteVideoGroup-5, 42 | .remoteVideoGroup.roomMode.remoteVideoGroup-6 { 43 | grid-template-columns: repeat(3, 1fr); 44 | grid-template-rows: repeat(3, 1fr); 45 | } 46 | 47 | .remoteVideoGroup.roomMode.remoteVideoGroup-7, 48 | .remoteVideoGroup.roomMode.remoteVideoGroup-8, 49 | .remoteVideoGroup.roomMode.remoteVideoGroup-9 { 50 | grid-template-columns: repeat(3, 1fr); 51 | grid-template-rows: repeat(3, 1fr); 52 | } 53 | 54 | .remoteVideoGroup.roomMode.remoteVideoGroup-10, 55 | .remoteVideoGroup.roomMode.remoteVideoGroup-11, 56 | .remoteVideoGroup.roomMode.remoteVideoGroup-12 { 57 | grid-template-columns: repeat(3, 1fr); 58 | grid-template-rows: repeat(4, 1fr); 59 | } 60 | 61 | .remoteVideoGroup.roomMode.remoteVideoGroup-13, 62 | .remoteVideoGroup.roomMode.remoteVideoGroup-14, 63 | .remoteVideoGroup.roomMode.remoteVideoGroup-15, 64 | .remoteVideoGroup.roomMode.remoteVideoGroup-16 { 65 | grid-template-columns: repeat(4, 1fr); 66 | grid-template-rows: repeat(4, 1fr); 67 | } 68 | 69 | /* Content share in room mode */ 70 | 71 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-0 { 72 | display: none; 73 | } 74 | 75 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-1, 76 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-2, 77 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-3, 78 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-4 { 79 | grid-template-columns: repeat(4, 1fr); 80 | grid-template-rows: repeat(1, 1fr); 81 | } 82 | 83 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-5, 84 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-6, 85 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-7, 86 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-8, 87 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-9, 88 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-10, 89 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-11, 90 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-12, 91 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-13, 92 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-14, 93 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-15, 94 | .remoteVideoGroup.roomMode.isContentShareEnabled.remoteVideoGroup-16 { 95 | grid-template-columns: repeat(8, 1fr); 96 | grid-template-rows: repeat(2, 1fr); 97 | } 98 | 99 | /* Screen share mode */ 100 | 101 | .remoteVideoGroup.screenShareMode.remoteVideoGroup-1, 102 | .remoteVideoGroup.screenShareMode.remoteVideoGroup-2, 103 | .remoteVideoGroup.screenShareMode.remoteVideoGroup-3, 104 | .remoteVideoGroup.screenShareMode.remoteVideoGroup-4 { 105 | grid-template-columns: repeat(1, 1fr); 106 | grid-template-rows: repeat(4, 1fr); 107 | } 108 | 109 | /* Child elements */ 110 | 111 | .instruction { 112 | position: absolute; 113 | top: 50%; 114 | left: 0; 115 | right: 0; 116 | text-align: center; 117 | color: var(--color_silver_chalice); 118 | } 119 | 120 | .remoteVideoGroup.screenShareMode .instruction { 121 | font-size: 1rem; 122 | } 123 | -------------------------------------------------------------------------------- /app/components/RemoteVideoGroup.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { VideoTileState } from 'amazon-chime-sdk-js'; 5 | import classNames from 'classnames/bind'; 6 | import React, { useCallback, useContext, useEffect, useState } from 'react'; 7 | import { FormattedMessage } from 'react-intl'; 8 | 9 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 10 | import getChimeContext from '../context/getChimeContext'; 11 | import ViewMode from '../enums/ViewMode'; 12 | import Size from '../enums/Size'; 13 | import useRaisedHandAttendees from '../hooks/useRaisedHandAttendees'; 14 | import RemoteVideo from './RemoteVideo'; 15 | import styles from './RemoteVideoGroup.css'; 16 | import useRoster from '../hooks/useRoster'; 17 | 18 | const cx = classNames.bind(styles); 19 | const MAX_REMOTE_VIDEOS = 16; 20 | 21 | type Props = { 22 | viewMode: ViewMode; 23 | isContentShareEnabled: boolean; 24 | }; 25 | 26 | export default function RemoteVideoGroup(props: Props) { 27 | const { viewMode, isContentShareEnabled } = props; 28 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 29 | const [visibleIndices, setVisibleIndices] = useState<{ 30 | [index: string]: { boundAttendeeId: string }; 31 | }>({}); 32 | const raisedHandAttendees = useRaisedHandAttendees(); 33 | const roster = useRoster(); 34 | const videoElements: HTMLVideoElement[] = []; 35 | const tiles: { [index: number]: number } = {}; 36 | 37 | const acquireVideoIndex = (tileId: number): number => { 38 | for (let index = 0; index < MAX_REMOTE_VIDEOS; index += 1) { 39 | if (tiles[index] === tileId) { 40 | return index; 41 | } 42 | } 43 | for (let index = 0; index < MAX_REMOTE_VIDEOS; index += 1) { 44 | if (!(index in tiles)) { 45 | tiles[index] = tileId; 46 | return index; 47 | } 48 | } 49 | throw new Error('no tiles are available'); 50 | }; 51 | 52 | const releaseVideoIndex = (tileId: number): number => { 53 | for (let index = 0; index < MAX_REMOTE_VIDEOS; index += 1) { 54 | if (tiles[index] === tileId) { 55 | delete tiles[index]; 56 | return index; 57 | } 58 | } 59 | return -1; 60 | }; 61 | 62 | const numberOfVisibleIndices = Object.keys(visibleIndices).reduce( 63 | (result: number, key: string) => result + (visibleIndices[key] ? 1 : 0), 64 | 0 65 | ); 66 | 67 | useEffect(() => { 68 | chime?.audioVideo?.addObserver({ 69 | videoTileDidUpdate: (tileState: VideoTileState): void => { 70 | if ( 71 | !tileState.boundAttendeeId || 72 | tileState.localTile || 73 | tileState.isContent || 74 | !tileState.tileId 75 | ) { 76 | return; 77 | } 78 | const index = acquireVideoIndex(tileState.tileId); 79 | chime?.audioVideo?.bindVideoElement( 80 | tileState.tileId, 81 | videoElements[index] 82 | ); 83 | setVisibleIndices(previousVisibleIndices => ({ 84 | ...previousVisibleIndices, 85 | [index]: { 86 | boundAttendeeId: tileState.boundAttendeeId 87 | } 88 | })); 89 | }, 90 | videoTileWasRemoved: (tileId: number): void => { 91 | const index = releaseVideoIndex(tileId); 92 | setVisibleIndices(previousVisibleIndices => ({ 93 | ...previousVisibleIndices, 94 | [index]: null 95 | })); 96 | } 97 | }); 98 | }, []); 99 | 100 | const getSize = (): Size => { 101 | if (numberOfVisibleIndices >= 10) { 102 | return Size.Small; 103 | } 104 | if (numberOfVisibleIndices >= 5) { 105 | return Size.Medium; 106 | } 107 | return Size.Large; 108 | }; 109 | 110 | return ( 111 |
122 | {numberOfVisibleIndices === 0 && ( 123 |
124 | 125 |
126 | )} 127 | {Array.from(Array(MAX_REMOTE_VIDEOS).keys()).map((key, index) => { 128 | const visibleIndex = visibleIndices[index]; 129 | const attendeeId = visibleIndex ? visibleIndex.boundAttendeeId : null; 130 | const raisedHand = raisedHandAttendees 131 | ? raisedHandAttendees.has(attendeeId) 132 | : false; 133 | const activeSpeaker = visibleIndex 134 | ? roster[visibleIndex.boundAttendeeId]?.active 135 | : false; 136 | return ( 137 | { 142 | if (element) { 143 | videoElements[index] = element; 144 | } 145 | }, [])} 146 | size={getSize()} 147 | attendeeId={attendeeId} 148 | raisedHand={raisedHand} 149 | activeSpeaker={activeSpeaker} 150 | isContentShareEnabled={isContentShareEnabled} 151 | /> 152 | ); 153 | })} 154 |
155 | ); 156 | } 157 | -------------------------------------------------------------------------------- /app/components/Root.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React from 'react'; 5 | import { hot } from 'react-hot-loader/root'; 6 | import { HashRouter } from 'react-router-dom'; 7 | 8 | import ChimeProvider from '../providers/ChimeProvider'; 9 | import I18nProvider from '../providers/I18nProvider'; 10 | import UIStateProvider from '../providers/UIStateProvider'; 11 | import Routes from '../Routes'; 12 | 13 | const Root = () => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | export default hot(Root); 26 | -------------------------------------------------------------------------------- /app/components/Roster.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .roster { 7 | color: var(--color_alabaster); 8 | height: 100%; 9 | display: flex; 10 | overflow-y: scroll; 11 | flex-direction: column; 12 | position: relative; 13 | border-top: 1px solid var(--color_mine_shaft_light); 14 | } 15 | 16 | .noAttendee { 17 | color: var(--color_silver_chalice); 18 | position: absolute; 19 | top: 50%; 20 | left: 50%; 21 | transform: translate(-50%, -50%); 22 | } 23 | 24 | .attendee { 25 | flex: 0 0 3rem; 26 | overflow: hidden; 27 | display: flex; 28 | align-items: center; 29 | height: 3rem; 30 | padding: 0 1rem; 31 | } 32 | 33 | .name { 34 | flex: 1 1 auto; 35 | white-space: nowrap; 36 | overflow: auto; 37 | text-overflow: ellipsis; 38 | } 39 | 40 | .weak-signal { 41 | color: var(--color_thunderbird); 42 | } 43 | 44 | .active-speaker { 45 | color: var(--color_green); 46 | } 47 | 48 | .raisedHand { 49 | font-size: 1.3rem; 50 | margin-left: 0.5rem; 51 | animation: shake 1.22s cubic-bezier(0.36, 0.07, 0.19, 0.97) infinite both; 52 | transform: translate3d(0, 0, 0); 53 | backface-visibility: hidden; 54 | perspective: 1000px; 55 | user-select: none; 56 | } 57 | 58 | .video { 59 | text-align: center; 60 | flex: 0 0 1.5rem; 61 | font-size: 0.9rem; 62 | margin-left: 0.5rem; 63 | width: 1.5rem; 64 | } 65 | 66 | .muted { 67 | text-align: center; 68 | flex: 0 0 1.5rem; 69 | font-size: 0.9rem; 70 | margin-left: 0.5rem; 71 | width: 1.5rem; 72 | } 73 | 74 | @keyframes shake { 75 | 10%, 76 | 90% { 77 | transform: translate3d(-0.5px, 0, 0); 78 | } 79 | 80 | 20%, 81 | 80% { 82 | transform: translate3d(1px, 0, 0); 83 | } 84 | 85 | 30%, 86 | 50%, 87 | 70% { 88 | transform: translate3d(-1.5px, 0, 0); 89 | } 90 | 91 | 40%, 92 | 60% { 93 | transform: translate3d(1.5px, 0, 0); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/components/Roster.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { VideoTileState } from 'amazon-chime-sdk-js'; 5 | import classNames from 'classnames/bind'; 6 | import React, { useContext, useEffect, useState } from 'react'; 7 | import { useIntl } from 'react-intl'; 8 | 9 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 10 | import getChimeContext from '../context/getChimeContext'; 11 | import useRoster from '../hooks/useRoster'; 12 | import useRaisedHandAttendees from '../hooks/useRaisedHandAttendees'; 13 | import RosterAttendeeType from '../types/RosterAttendeeType'; 14 | import styles from './Roster.css'; 15 | 16 | const cx = classNames.bind(styles); 17 | 18 | export default function Roster() { 19 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 20 | const roster = useRoster(); 21 | const [videoAttendees, setVideoAttendees] = useState(new Set()); 22 | const raisedHandAttendees = useRaisedHandAttendees(); 23 | const intl = useIntl(); 24 | 25 | useEffect(() => { 26 | const tileIds: { [tileId: number]: string } = {}; 27 | // 28 | const realTimeVideoAttendees = new Set(); 29 | 30 | const removeTileId = (tileId: number): void => { 31 | const removedAttendeeId = tileIds[tileId]; 32 | delete tileIds[tileId]; 33 | realTimeVideoAttendees.delete(removedAttendeeId); 34 | setVideoAttendees(new Set(realTimeVideoAttendees)); 35 | }; 36 | 37 | chime?.audioVideo?.addObserver({ 38 | videoTileDidUpdate: (tileState: VideoTileState): void => { 39 | if ( 40 | !tileState.boundAttendeeId || 41 | tileState.isContent || 42 | !tileState.tileId 43 | ) { 44 | return; 45 | } 46 | 47 | if (tileState.active) { 48 | tileIds[tileState.tileId] = tileState.boundAttendeeId; 49 | realTimeVideoAttendees.add(tileState.boundAttendeeId); 50 | setVideoAttendees(new Set(realTimeVideoAttendees)); 51 | } else { 52 | removeTileId(tileState.tileId); 53 | } 54 | }, 55 | videoTileWasRemoved: (tileId: number): void => { 56 | removeTileId(tileId); 57 | } 58 | }); 59 | }, []); 60 | 61 | let attendeeIds; 62 | if (chime?.meetingSession && roster) { 63 | attendeeIds = Object.keys(roster).filter(attendeeId => { 64 | return !!roster[attendeeId].name; 65 | }); 66 | } 67 | 68 | return ( 69 |
70 | {attendeeIds && 71 | attendeeIds.map((attendeeId: string) => { 72 | const rosterAttendee: RosterAttendeeType = roster[attendeeId]; 73 | return ( 74 |
75 |
{rosterAttendee.name}
76 | {raisedHandAttendees.has(attendeeId) && ( 77 |
78 | 89 | ✋ 90 | 91 |
92 | )} 93 | {videoAttendees.has(attendeeId) && ( 94 |
95 | 96 |
97 | )} 98 | {typeof rosterAttendee.muted === 'boolean' && ( 99 |
100 | {rosterAttendee.muted ? ( 101 | 102 | ) : ( 103 | 114 | )} 115 |
116 | )} 117 |
118 | ); 119 | })} 120 |
121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /app/components/ScreenPicker.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .screenPicker { 7 | display: flex; 8 | flex-direction: column; 9 | margin: auto; 10 | width: var(--screen_picker_width); 11 | height: var(--screen_picker_height); 12 | background: var(--color_mine_shaft_light); 13 | border-radius: 0.25rem; 14 | overflow: hidden; 15 | } 16 | 17 | .top { 18 | flex: 0 0 auto; 19 | } 20 | 21 | .header { 22 | font-size: 1.5rem; 23 | font-weight: 400; 24 | padding: 1rem 1.5rem; 25 | margin: 0; 26 | } 27 | 28 | .tabs { 29 | display: flex; 30 | padding: 0 1.5rem; 31 | } 32 | 33 | .screenTab, 34 | .windowTab { 35 | border: none; 36 | border-bottom: 0.125rem solid transparent; 37 | user-select: none; 38 | cursor: pointer; 39 | transition: color 0.1s; 40 | padding: 0 0 0.25rem; 41 | color: var(--color_silver_chalice); 42 | background: transparent; 43 | outline: none; 44 | font-size: 1rem; 45 | } 46 | 47 | .screenTab { 48 | margin-left: 1rem; 49 | } 50 | 51 | .screenTab.selected, 52 | .windowTab.selected { 53 | color: var(--color_alabaster); 54 | border-bottom: 0.125rem solid var(--color_alabaster); 55 | } 56 | 57 | .screenTab:hover, 58 | .windowTab:hover { 59 | color: var(--color_alabaster); 60 | } 61 | 62 | .middle { 63 | flex: 1 1 auto; 64 | overflow-y: scroll; 65 | background: var(--color_mine_shaft_medium); 66 | padding: 1.5rem; 67 | display: grid; 68 | grid-column-gap: 4rem; 69 | grid-template-columns: repeat(2, 1fr); 70 | grid-template-rows: minmax(min-content, max-content); 71 | } 72 | 73 | .middle.loading { 74 | display: flex; 75 | width: 100%; 76 | height: 100%; 77 | justify-content: center; 78 | align-items: center; 79 | } 80 | 81 | .noScreen { 82 | color: var(--color_silver_chalice); 83 | position: absolute; 84 | top: 50%; 85 | left: 50%; 86 | transform: translate(-50%, -50%); 87 | } 88 | 89 | .source { 90 | display: flex; 91 | flex-direction: column; 92 | min-width: 0; 93 | padding: 1rem; 94 | cursor: pointer; 95 | margin-bottom: 1rem; 96 | outline: none; 97 | } 98 | 99 | .source:hover { 100 | box-shadow: 0 0 0 0.5rem var(--color_silver_chalice); 101 | } 102 | 103 | .source.selected { 104 | box-shadow: 0 0 0 0.5rem var(--color_alabaster) !important; 105 | } 106 | 107 | .image { 108 | flex-direction: column; 109 | background-repeat: no-repeat; 110 | background-size: contain; 111 | background-position: center; 112 | position: relative; 113 | height: 12rem; 114 | } 115 | 116 | .image img { 117 | max-height: 100%; 118 | max-width: 100%; 119 | margin: auto; 120 | position: absolute; 121 | transform: translate(-50%, -50%); 122 | top: 50%; 123 | left: 50%; 124 | } 125 | 126 | .caption { 127 | width: 100%; 128 | white-space: nowrap; 129 | overflow: auto; 130 | text-overflow: ellipsis; 131 | text-align: center; 132 | font-size: 1rem; 133 | padding: 1rem 1rem 0; 134 | } 135 | 136 | .bottom { 137 | display: flex; 138 | flex: 0 0 5rem; 139 | justify-content: flex-end; 140 | align-items: center; 141 | } 142 | 143 | .buttons { 144 | display: flex; 145 | margin-left: auto; 146 | align-items: center; 147 | padding: 0 1.5rem; 148 | } 149 | 150 | .cancelButton, 151 | .shareButton { 152 | border-radius: 0.25rem; 153 | padding: 0.75rem; 154 | font-size: 1rem; 155 | font-weight: 500; 156 | user-select: none; 157 | width: 6rem; 158 | border: 1px solid var(--color_alabaster); 159 | } 160 | 161 | .shareButton { 162 | color: var(--color_mine_shaft_light); 163 | background: var(--color_alabaster); 164 | opacity: 0.25; 165 | margin-left: 1rem; 166 | } 167 | 168 | .shareButton.enabled { 169 | opacity: 1; 170 | cursor: pointer; 171 | } 172 | 173 | .cancelButton { 174 | color: var(--color_alabaster); 175 | border-color: var(--color_alabaster); 176 | background: transparent; 177 | cursor: pointer; 178 | } 179 | 180 | .cancelButton:hover, 181 | .shareButton.enabled:hover { 182 | opacity: 0.8; 183 | } 184 | -------------------------------------------------------------------------------- /app/components/ScreenPicker.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import { desktopCapturer, DesktopCapturerSource } from 'electron'; 6 | import React, { useEffect, useState } from 'react'; 7 | import { FormattedMessage } from 'react-intl'; 8 | 9 | import LoadingSpinner from './LoadingSpinner'; 10 | import styles from './ScreenPicker.css'; 11 | 12 | const cx = classNames.bind(styles); 13 | 14 | enum ShareType { 15 | Screen, 16 | Window 17 | } 18 | 19 | type Props = { 20 | onClickShareButton: (selectedSourceId: string) => void; 21 | onClickCancelButton: () => void; 22 | }; 23 | 24 | export default function ScreenPicker(props: Props) { 25 | const { onClickCancelButton, onClickShareButton } = props; 26 | const [sources, setSources] = useState(null); 27 | const [shareType, setShareType] = useState(ShareType.Window); 28 | const [selectedSourceId, setSelectedSourceId] = useState(null); 29 | 30 | useEffect(() => { 31 | desktopCapturer 32 | .getSources({ 33 | types: ['screen', 'window'], 34 | thumbnailSize: { width: 600, height: 600 } 35 | }) 36 | .then(async (desktopCapturerSources: DesktopCapturerSource[]) => { 37 | setSources(desktopCapturerSources); 38 | return null; 39 | }) 40 | .catch(error => { 41 | // eslint-disable-next-line 42 | console.error(error); 43 | }); 44 | }, []); 45 | 46 | const { screenSources, windowSources } = ( 47 | sources || ([] as DesktopCapturerSource[]) 48 | ).reduce( 49 | ( 50 | result: { 51 | screenSources: DesktopCapturerSource[]; 52 | windowSources: DesktopCapturerSource[]; 53 | }, 54 | source: DesktopCapturerSource 55 | ) => { 56 | if (source.name === document.title) { 57 | return result; 58 | } 59 | 60 | if (source.id.startsWith('screen')) { 61 | result.screenSources.push(source); 62 | } else { 63 | result.windowSources.push(source); 64 | } 65 | return result; 66 | }, 67 | { 68 | screenSources: [] as DesktopCapturerSource[], 69 | windowSources: [] as DesktopCapturerSource[] 70 | } 71 | ); 72 | 73 | const selectedSources = 74 | shareType === ShareType.Screen ? screenSources : windowSources; 75 | 76 | return ( 77 |
78 |
79 |

80 | 81 |

82 |
83 | 94 | 105 |
106 |
107 |
112 | {!sources && } 113 | {sources && selectedSources && !selectedSources.length && ( 114 |
115 | 116 |
117 | )} 118 | {sources && 119 | selectedSources && 120 | selectedSources.map(source => ( 121 |
{ 127 | setSelectedSourceId(source.id); 128 | }} 129 | onKeyPress={() => {}} 130 | role="button" 131 | tabIndex={0} 132 | > 133 |
134 | {source.name} 135 |
136 |
{source.name}
137 |
138 | ))} 139 |
140 |
141 |
142 | 151 | 165 |
166 |
167 |
168 | ); 169 | } 170 | -------------------------------------------------------------------------------- /app/components/ScreenShareHeader.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .screenShareHeader { 7 | display: flex; 8 | flex: 0 0 auto; 9 | justify-content: center; 10 | align-items: center; 11 | padding: 0.25rem 0.25rem 0.5rem; 12 | flex-direction: column; 13 | } 14 | 15 | .stopButton { 16 | color: var(--color_alabaster); 17 | background: var(--color_thunderbird); 18 | border: none; 19 | border-radius: 0.25rem; 20 | padding: 0.5rem 0; 21 | font-size: 1rem; 22 | user-select: none; 23 | cursor: pointer; 24 | width: 100%; 25 | transition: opacity 0.15s; 26 | } 27 | 28 | .stopButton:hover { 29 | opacity: 0.8; 30 | } 31 | 32 | .description { 33 | margin-top: 0.25rem; 34 | color: var(--color_silver_chalice); 35 | } 36 | -------------------------------------------------------------------------------- /app/components/ScreenShareHeader.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React from 'react'; 6 | import { FormattedMessage } from 'react-intl'; 7 | 8 | import useRoster from '../hooks/useRoster'; 9 | import styles from './ScreenShareHeader.css'; 10 | 11 | const cx = classNames.bind(styles); 12 | 13 | type Props = { 14 | onClickStopButton: () => void; 15 | }; 16 | 17 | export default function ScreenShareHeader(props: Props) { 18 | const roster = useRoster(); 19 | const { onClickStopButton } = props; 20 | return ( 21 |
22 | 29 |
30 | {roster ? ( 31 | 37 | ) : ( 38 | ` ` 39 | )} 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/components/Tooltip.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .tooltip[class~='rc-tooltip-placement-top'] div[class~='rc-tooltip-arrow'], 7 | .tooltip[class~='rc-tooltip-placement-topLeft'] div[class~='rc-tooltip-arrow'], 8 | .tooltip[class~='rc-tooltip-placement-topRight'] 9 | div[class~='rc-tooltip-arrow'] { 10 | border-top-color: var(--color_cod_gray_medium); 11 | } 12 | 13 | .tooltip *[class~='rc-tooltip-inner'] { 14 | background-color: var(--color_cod_gray_medium); 15 | font-size: 1rem; 16 | } 17 | -------------------------------------------------------------------------------- /app/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /* eslint-disable */ 5 | // Example code from react-popper-tooltip 6 | 7 | import classNames from 'classnames/bind'; 8 | import React from 'react'; 9 | import RcTooltip from 'rc-tooltip'; 10 | 11 | import styles from './Tooltip.css'; 12 | 13 | const cx = classNames.bind(styles); 14 | 15 | const Tooltip = ({ children, tooltip }: { children: React.ReactElement, tooltip: string }) => ( 16 | {tooltip}
}> 17 | {children} 18 | 19 | ); 20 | 21 | export default Tooltip; 22 | -------------------------------------------------------------------------------- /app/components/VideoNameplate.css: -------------------------------------------------------------------------------- 1 | .videoNameplate { 2 | z-index: 1; 3 | align-items: center; 4 | position: absolute; 5 | max-width: 95%; 6 | left: 50%; 7 | transform: translate(-50%, 0); 8 | background-color: var(--color_black_medium_opacity); 9 | backdrop-filter: blur(0.5rem); 10 | } 11 | 12 | .videoNameplate.roomMode { 13 | display: flex; 14 | } 15 | 16 | .videoNameplate.screenShareMode { 17 | display: none; 18 | } 19 | 20 | .videoNameplate.small { 21 | padding: 0.2rem 0.2rem 0.2rem 0.3rem; 22 | bottom: 0.25rem; 23 | font-size: 0.75rem; 24 | border-radius: 0.25rem; 25 | } 26 | 27 | .videoNameplate.medium { 28 | padding: 0.25rem 0.3rem 0.25rem 0.5rem; 29 | bottom: 0.25rem; 30 | font-size: 0.8rem; 31 | border-radius: 0.5rem; 32 | } 33 | 34 | .videoNameplate.large { 35 | padding: 0.5rem 0.75em 0.5em 1rem; 36 | bottom: 0.5rem; 37 | font-size: 1rem; 38 | border-radius: 0.5rem; 39 | } 40 | 41 | .videoNameplate.roomMode.isContentShareEnabled { 42 | display: none; 43 | } 44 | 45 | .videoNameplate.roomMode.isContentShareEnabled.large { 46 | display: flex !important; 47 | padding: 0.2rem 0.2rem 0.2rem 0.3rem; 48 | bottom: 0.25rem; 49 | font-size: 0.75rem; 50 | border-radius: 0.25rem; 51 | } 52 | 53 | .videoNameplate.screenShareMode.large { 54 | display: flex !important; 55 | padding: 0.2rem 0.2rem 0.2rem 0.3rem; 56 | bottom: 0.25rem; 57 | font-size: 0.75rem; 58 | border-radius: 0.25rem; 59 | } 60 | 61 | .name { 62 | flex: 1 1 auto; 63 | white-space: nowrap; 64 | overflow: auto; 65 | text-overflow: ellipsis; 66 | } 67 | 68 | .muted { 69 | flex: 0 0 1.25rem; 70 | width: 1.25rem; 71 | text-align: center; 72 | } 73 | 74 | .videoNameplate.small .name + .muted, 75 | .videoNameplate.medium .name + .muted { 76 | margin-left: 0.25rem; 77 | } 78 | 79 | .videoNameplate.large .name + .muted { 80 | margin-left: 0.5rem; 81 | } 82 | -------------------------------------------------------------------------------- /app/components/VideoNameplate.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import classNames from 'classnames/bind'; 5 | import React from 'react'; 6 | 7 | import ViewMode from '../enums/ViewMode'; 8 | import useAttendee from '../hooks/useAttendee'; 9 | import Size from '../enums/Size'; 10 | import styles from './VideoNameplate.css'; 11 | 12 | const cx = classNames.bind(styles); 13 | 14 | type Props = { 15 | viewMode: ViewMode; 16 | size: Size; 17 | isContentShareEnabled: boolean; 18 | attendeeId: string | null; 19 | }; 20 | 21 | export default function VideoNameplate(props: Props) { 22 | const { viewMode, size, attendeeId, isContentShareEnabled } = props; 23 | if (!attendeeId) { 24 | return <>; 25 | } 26 | 27 | const attendee = useAttendee(attendeeId); 28 | if (!attendee.name || typeof !attendee.muted !== 'boolean') { 29 | return <>; 30 | } 31 | 32 | const { name, muted } = attendee; 33 | return ( 34 |
44 |
{name}
45 |
46 | {muted ? ( 47 | 48 | ) : ( 49 | 50 | )} 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /app/components/css.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | declare module '*.scss' { 5 | const content: { [className: string]: string }; 6 | export default content; 7 | } 8 | 9 | declare module '*.css' { 10 | const content: { [className: string]: string }; 11 | export default content; 12 | } 13 | -------------------------------------------------------------------------------- /app/constants/localStorageKeys.json: -------------------------------------------------------------------------------- 1 | { 2 | "CLASS_MODE": "CLASS_MODE" 3 | } 4 | -------------------------------------------------------------------------------- /app/constants/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "HOME": "/", 3 | "LOGIN": "/login", 4 | "CREATE_OR_JOIN": "/create-or-join", 5 | "CLASSROOM": "/classroom" 6 | } 7 | -------------------------------------------------------------------------------- /app/context/getChimeContext.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React from 'react'; 5 | 6 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 7 | 8 | const context = React.createContext(null); 9 | 10 | export default function getChimeContext() { 11 | return context; 12 | } 13 | -------------------------------------------------------------------------------- /app/context/getMeetingStatusContext.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React from 'react'; 5 | 6 | import MeetingStatus from '../enums/MeetingStatus'; 7 | 8 | const context = React.createContext<{ 9 | meetingStatus: MeetingStatus; 10 | errorMessage?: string; 11 | }>({ 12 | meetingStatus: MeetingStatus.Loading 13 | }); 14 | 15 | export default function getMeetingStatusContext() { 16 | return context; 17 | } 18 | -------------------------------------------------------------------------------- /app/context/getRosterContext.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React from 'react'; 5 | 6 | import RosterType from '../types/RosterType'; 7 | 8 | const context = React.createContext({}); 9 | 10 | export default function getRosterContext() { 11 | return context; 12 | } 13 | -------------------------------------------------------------------------------- /app/context/getUIStateContext.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React, { Dispatch } from 'react'; 5 | 6 | import localStorageKeys from '../constants/localStorageKeys.json'; 7 | import ClassMode from '../enums/ClassMode'; 8 | 9 | export interface StateType { 10 | classMode: ClassMode | null; 11 | } 12 | 13 | export interface Action { 14 | type: string; 15 | } 16 | 17 | export interface SetClassModeActon extends Action { 18 | payload: { 19 | classMode: ClassMode | null; 20 | }; 21 | } 22 | 23 | let classMode: ClassMode = 24 | localStorage.getItem(localStorageKeys.CLASS_MODE) === 'Teacher' 25 | ? ClassMode.Teacher 26 | : ClassMode.Student; 27 | if (!classMode) { 28 | localStorage.setItem(localStorageKeys.CLASS_MODE, ClassMode.Student); 29 | classMode = ClassMode.Student; 30 | } 31 | 32 | export const initialState: StateType = { 33 | classMode 34 | }; 35 | 36 | const context = React.createContext<[StateType, Dispatch]>([ 37 | initialState, 38 | (): void => {} 39 | ]); 40 | 41 | export default function getUIStateContext() { 42 | return context; 43 | } 44 | -------------------------------------------------------------------------------- /app/enums/ClassMode.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | enum ClassMode { 5 | Teacher = 'Teacher', 6 | Student = 'Student' 7 | } 8 | 9 | export default ClassMode; 10 | -------------------------------------------------------------------------------- /app/enums/MeetingStatus.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | enum MeetingStatus { 5 | Loading, 6 | Succeeded, 7 | Failed 8 | } 9 | 10 | export default MeetingStatus; 11 | -------------------------------------------------------------------------------- /app/enums/MessageTopic.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | enum MessageTopic { 5 | Chat = 'chat-message', 6 | RaiseHand = 'raise-hand', 7 | DismissHand = 'dismiss-hand', 8 | Focus = 'focus' 9 | } 10 | 11 | export default MessageTopic; 12 | -------------------------------------------------------------------------------- /app/enums/OptionalFeature.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | enum OptionalFeature { 5 | None = '', 6 | Simulcast = 'simulcast' 7 | } 8 | 9 | export default OptionalFeature; 10 | -------------------------------------------------------------------------------- /app/enums/Size.ts: -------------------------------------------------------------------------------- 1 | enum Size { 2 | Small, 3 | Medium, 4 | Large 5 | } 6 | 7 | export default Size; 8 | -------------------------------------------------------------------------------- /app/enums/ViewMode.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | enum ViewMode { 5 | Room, 6 | ScreenShare 7 | } 8 | 9 | export default ViewMode; 10 | -------------------------------------------------------------------------------- /app/hooks/useAttendee.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useContext, useEffect, useState } from 'react'; 5 | 6 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 7 | import getChimeContext from '../context/getChimeContext'; 8 | import RosterType from '../types/RosterType'; 9 | import RosterAttendeeType from '../types/RosterAttendeeType'; 10 | 11 | export default function useAttendee(attendeeId: string) { 12 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 13 | const [attendee, setAttendee] = useState( 14 | chime?.roster[attendeeId] || {} 15 | ); 16 | useEffect(() => { 17 | let previousRosterAttendee: RosterAttendeeType | null = null; 18 | const callback = (newRoster: RosterType) => { 19 | const rosterAttendee = newRoster[attendeeId] 20 | ? ({ ...newRoster[attendeeId] } as RosterAttendeeType) 21 | : null; 22 | 23 | // In the classroom demo, we don't subscribe to volume and signal strength changes. 24 | // The VideoNameplate component that uses this hook will re-render only when name and muted status change. 25 | if (rosterAttendee) { 26 | if ( 27 | !previousRosterAttendee || 28 | previousRosterAttendee.name !== rosterAttendee.name || 29 | previousRosterAttendee.muted !== rosterAttendee.muted 30 | ) { 31 | setAttendee(rosterAttendee); 32 | } 33 | } 34 | previousRosterAttendee = rosterAttendee; 35 | }; 36 | chime?.subscribeToRosterUpdate(callback); 37 | return () => { 38 | chime?.unsubscribeFromRosterUpdate(callback); 39 | }; 40 | }, []); 41 | return attendee; 42 | } 43 | -------------------------------------------------------------------------------- /app/hooks/useDevices.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useContext, useEffect, useState } from 'react'; 5 | 6 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 7 | import getChimeContext from '../context/getChimeContext'; 8 | import FullDeviceInfoType from '../types/FullDeviceInfoType'; 9 | 10 | export default function useDevices() { 11 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 12 | const [deviceSwitcherState, setDeviceUpdated] = useState({ 13 | currentAudioInputDevice: chime?.currentAudioInputDevice, 14 | currentAudioOutputDevice: chime?.currentAudioOutputDevice, 15 | currentVideoInputDevice: chime?.currentVideoInputDevice, 16 | audioInputDevices: chime?.audioInputDevices, 17 | audioOutputDevices: chime?.audioOutputDevices, 18 | videoInputDevices: chime?.videoInputDevices 19 | }); 20 | useEffect(() => { 21 | const devicesUpdatedCallback = (fullDeviceInfo: FullDeviceInfoType) => { 22 | setDeviceUpdated({ 23 | ...fullDeviceInfo 24 | }); 25 | }; 26 | 27 | chime?.subscribeToDevicesUpdated(devicesUpdatedCallback); 28 | return () => { 29 | chime?.unsubscribeFromDevicesUpdated(devicesUpdatedCallback); 30 | }; 31 | }, []); 32 | return deviceSwitcherState; 33 | } 34 | -------------------------------------------------------------------------------- /app/hooks/useFocusMode.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useContext, useEffect, useState } from 'react'; 5 | 6 | import { DataMessage } from 'amazon-chime-sdk-js'; 7 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 8 | import getChimeContext from '../context/getChimeContext'; 9 | import getUIStateContext from '../context/getUIStateContext'; 10 | import ClassMode from '../enums/ClassMode'; 11 | import MessageTopic from '../enums/MessageTopic'; 12 | 13 | export default function useFocusMode() { 14 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 15 | const [focusMode, setFocusMode] = useState(false); 16 | const [state] = useContext(getUIStateContext()); 17 | useEffect(() => { 18 | const callback = (message: DataMessage) => { 19 | if (state.classMode === ClassMode.Teacher) { 20 | return; 21 | } 22 | const { focus } = message.json(); 23 | chime?.audioVideo?.realtimeSetCanUnmuteLocalAudio(!focus); 24 | if (focus) { 25 | chime?.audioVideo?.realtimeMuteLocalAudio(); 26 | } 27 | setFocusMode(!!focus); 28 | }; 29 | const focusMessageUpdateCallback = { topic: MessageTopic.Focus, callback }; 30 | chime?.subscribeToMessageUpdate(focusMessageUpdateCallback); 31 | return () => { 32 | chime?.unsubscribeFromMessageUpdate(focusMessageUpdateCallback); 33 | }; 34 | }, []); 35 | return focusMode; 36 | } 37 | -------------------------------------------------------------------------------- /app/hooks/useRaisedHandAttendees.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useContext, useEffect, useState } from 'react'; 5 | 6 | import { DataMessage } from 'amazon-chime-sdk-js'; 7 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 8 | import getChimeContext from '../context/getChimeContext'; 9 | import MessageTopic from '../enums/MessageTopic'; 10 | 11 | export default function useRaisedHandAttendees() { 12 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 13 | const [raisedHandAttendees, setRaisedHandAttendees] = useState(new Set()); 14 | useEffect(() => { 15 | const realTimeRaisedHandAttendees = new Set(); 16 | const callback = (message: DataMessage) => { 17 | const attendeeId = message.text(); 18 | if (attendeeId) { 19 | if (message.topic === MessageTopic.RaiseHand) { 20 | realTimeRaisedHandAttendees.add(attendeeId); 21 | } else if (message.topic === MessageTopic.DismissHand) { 22 | realTimeRaisedHandAttendees.delete(attendeeId); 23 | } 24 | setRaisedHandAttendees(new Set(realTimeRaisedHandAttendees)); 25 | } 26 | }; 27 | const raiseHandMessageUpdateCallback = { 28 | topic: MessageTopic.RaiseHand, 29 | callback 30 | }; 31 | chime?.subscribeToMessageUpdate(raiseHandMessageUpdateCallback); 32 | return () => { 33 | chime?.unsubscribeFromMessageUpdate(raiseHandMessageUpdateCallback); 34 | }; 35 | }, []); 36 | return raisedHandAttendees; 37 | } 38 | -------------------------------------------------------------------------------- /app/hooks/useRoster.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useContext, useEffect, useState } from 'react'; 5 | 6 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 7 | import getChimeContext from '../context/getChimeContext'; 8 | import RosterType from '../types/RosterType'; 9 | 10 | export default function useRoster() { 11 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 12 | const [roster, setRoster] = useState(chime?.roster || {}); 13 | useEffect(() => { 14 | const callback = (newRoster: RosterType) => { 15 | setRoster({ 16 | ...newRoster 17 | } as RosterType); 18 | }; 19 | chime?.subscribeToRosterUpdate(callback); 20 | return () => { 21 | chime?.unsubscribeFromRosterUpdate(callback); 22 | }; 23 | }, []); 24 | return roster; 25 | } 26 | -------------------------------------------------------------------------------- /app/i18n/en-US.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export default { 5 | 'Login.title': `Tell me about you`, 6 | 'Login.teacherTitle': `Teachers can`, 7 | 'Login.teacherDescription1': `Create a classroom`, 8 | 'Login.teacherDescription2': `Share audio, video, and screen`, 9 | 'Login.teacherDescription3': `Send chat messages`, 10 | 'Login.teacherDescription4': `Toggle focus:`, 11 | 'Login.teacherToggleDescription1': `Focus mutes all students`, 12 | 'Login.teacherToggleDescription2': `Focus turns off student chat`, 13 | 'Login.teacherButton': `I'm a teacher`, 14 | 15 | 'Login.studentTitle': `Students can`, 16 | 'Login.studentDescription1': `Join a classroom`, 17 | 'Login.studentDescription2': `Share video`, 18 | 'Login.studentDescription3': `Raise hand`, 19 | 'Login.studentDescription4': `When focus is off:`, 20 | 'Login.studentToggleDescription1': `Unmute and share audio`, 21 | 'Login.studentToggleDescription2': `Send chat messages`, 22 | 'Login.studentButton': `I'm a student`, 23 | 24 | 'CreateOrJoin.teacherTitle': `Create or join a classroom`, 25 | 'CreateOrJoin.studentTitle': `Join a classroom`, 26 | 'CreateOrJoin.titlePlaceholder': `Classroom`, 27 | 'CreateOrJoin.namePlaceholder': `Your name`, 28 | 'CreateOrJoin.continueButton': `Continue`, 29 | 'CreateOrJoin.notTeacherLink': `Not a teacher? Click here.`, 30 | 'CreateOrJoin.notStudentLink': `Not a student? Click here.`, 31 | 'CreateOrJoin.classRoomDoesNotExist': `Classroom does not exist`, 32 | 'CreateOrJoin.serverError': `Server error`, 33 | 34 | 'Classroom.classroom': `Classroom`, 35 | 36 | 'RemoteVideoGroup.noVideo': `No one is sharing video`, 37 | 38 | 'DeviceSwitcher.noAudioInputPlaceholder': `No microphone`, 39 | 'DeviceSwitcher.noAudioOutputPlaceholder': `No speaker`, 40 | 'DeviceSwitcher.noVideoInputPlaceholder': `No video device`, 41 | 42 | 'Controls.turnOffFocusTooltip': `Turn off focus`, 43 | 'Controls.turnOnFocusTooltip': `Turn on focus`, 44 | 'Controls.unmuteTooltip': `Unmute`, 45 | 'Controls.muteTooltip': `Mute`, 46 | 'Controls.turnOnVideoTooltip': `Turn on video`, 47 | 'Controls.turnOffVideoTooltip': `Turn off video`, 48 | 'Controls.shareScreenTooltip': `Share screen`, 49 | 'Controls.endClassroomTooltip': `End classroom`, 50 | 'Controls.leaveClassroomTooltip': `Leave classroom`, 51 | 'Controls.micMutedInScreenViewMode': `Mic muted`, 52 | 'Controls.focusOnMessage': `Focus on`, 53 | 'Controls.focusOffMessage': `Focus off`, 54 | 55 | 'ScreenPicker.title': `Share your screen`, 56 | 'ScreenPicker.applicationWindowTab': `Application window`, 57 | 'ScreenPicker.yourEntireScreenTab': `Your entire screen`, 58 | 'ScreenPicker.noScreen': `No screen`, 59 | 'ScreenPicker.cancel': `Cancel`, 60 | 'ScreenPicker.share': `Share`, 61 | 62 | 'ScreenShareHeader.stopSharing': `Stop sharing`, 63 | 'ScreenShareHeader.online': `{number} online`, 64 | 65 | 'ChatInput.inputPlaceholder': `Type a chat message`, 66 | 'ChatInput.raiseHandAriaLabel': `Raise hand`, 67 | 68 | 'Roster.raiseHandAriaLabel': `Raised hand by {name}`, 69 | 70 | 'RemoteVideo.raiseHandAriaLabel': `Raised hand`, 71 | 72 | 'CPUUsage.getting': `Getting CPU usage...` 73 | }; 74 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React, { Fragment } from 'react'; 5 | import { render } from 'react-dom'; 6 | import { AppContainer as ReactHotAppContainer } from 'react-hot-loader'; 7 | 8 | import './app.global.css'; 9 | import Root from './components/Root'; 10 | 11 | const AppContainer = process.env.PLAIN_HMR ? Fragment : ReactHotAppContainer; 12 | 13 | document.addEventListener('DOMContentLoaded', () => 14 | render( 15 | 16 | 17 | , 18 | document.getElementById('root') 19 | ) 20 | ); 21 | -------------------------------------------------------------------------------- /app/main.dev.ts: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, no-console: off */ 2 | 3 | /** 4 | * This module executes inside of electron's main process. You can start 5 | * electron renderer process from here and communicate with the other processes 6 | * through IPC. 7 | * 8 | * When running `yarn build` or `yarn build-main`, this file is compiled to 9 | * `./app/main.prod.js` using webpack. This gives us some performance wins. 10 | */ 11 | import { app, BrowserWindow, ipcMain } from 'electron'; 12 | import log from 'electron-log'; 13 | import { autoUpdater } from 'electron-updater'; 14 | import path from 'path'; 15 | 16 | import MenuBuilder from './menu'; 17 | 18 | export default class AppUpdater { 19 | constructor() { 20 | log.transports.file.level = 'info'; 21 | autoUpdater.logger = log; 22 | autoUpdater.checkForUpdatesAndNotify(); 23 | } 24 | } 25 | 26 | let mainWindow: BrowserWindow | null = null; 27 | 28 | if (process.env.NODE_ENV === 'production') { 29 | const sourceMapSupport = require('source-map-support'); 30 | sourceMapSupport.install(); 31 | } 32 | 33 | if ( 34 | process.env.NODE_ENV === 'development' || 35 | process.env.DEBUG_PROD === 'true' 36 | ) { 37 | require('electron-debug')(); 38 | } 39 | 40 | const installExtensions = async () => { 41 | const installer = require('electron-devtools-installer'); 42 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS; 43 | const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; 44 | 45 | return Promise.all( 46 | extensions.map(name => installer.default(installer[name], forceDownload)) 47 | ).catch(console.log); 48 | }; 49 | 50 | const createWindow = async () => { 51 | if ( 52 | process.env.NODE_ENV === 'development' || 53 | process.env.DEBUG_PROD === 'true' 54 | ) { 55 | await installExtensions(); 56 | } 57 | 58 | const defaultWidth = 1024; 59 | const defaultHeight = 768; 60 | 61 | mainWindow = new BrowserWindow({ 62 | show: false, 63 | width: defaultWidth, 64 | height: defaultHeight, 65 | center: true, 66 | minWidth: defaultWidth, 67 | minHeight: defaultHeight, 68 | backgroundColor: '#252525', 69 | fullscreenable: false, 70 | webPreferences: 71 | process.env.NODE_ENV === 'development' || process.env.E2E_BUILD === 'true' 72 | ? { 73 | nodeIntegration: true 74 | } 75 | : { 76 | preload: path.join(__dirname, 'dist/renderer.prod.js') 77 | } 78 | }); 79 | 80 | mainWindow.loadURL(`file://${__dirname}/app.html`); 81 | 82 | // @TODO: Use 'ready-to-show' event 83 | // https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event 84 | mainWindow.webContents.on('did-finish-load', () => { 85 | if (!mainWindow) { 86 | throw new Error('"mainWindow" is not defined'); 87 | } 88 | if (process.env.START_MINIMIZED) { 89 | mainWindow.minimize(); 90 | } else { 91 | mainWindow.show(); 92 | mainWindow.focus(); 93 | } 94 | }); 95 | 96 | mainWindow.on('closed', () => { 97 | mainWindow = null; 98 | }); 99 | 100 | const menuBuilder = new MenuBuilder(mainWindow); 101 | menuBuilder.buildMenu(); 102 | 103 | // Remove this if your app does not use auto updates 104 | // eslint-disable-next-line 105 | new AppUpdater(); 106 | 107 | ipcMain.on('chime-enable-screen-share-mode', event => { 108 | if (!mainWindow) { 109 | // eslint-disable-next-line 110 | console.error('"mainWindow" is not defined'); 111 | return; 112 | } 113 | 114 | const windowWidth = 150; 115 | const windowHeight = defaultHeight; 116 | mainWindow.setAlwaysOnTop(true, 'floating'); 117 | mainWindow.setMinimumSize(windowWidth, windowHeight); 118 | mainWindow.setSize(windowWidth, windowHeight); 119 | mainWindow.setPosition(32, 64); 120 | mainWindow.resizable = false; 121 | mainWindow.minimizable = false; 122 | mainWindow.maximizable = false; 123 | if (typeof mainWindow.setWindowButtonVisibility === 'function') { 124 | mainWindow.setWindowButtonVisibility(false); 125 | } 126 | // In macOS Electron, long titles may be truncated. 127 | mainWindow.setTitle('MyClassroom'); 128 | 129 | event.reply('chime-enable-screen-share-mode-ack'); 130 | }); 131 | 132 | ipcMain.on('chime-disable-screen-share-mode', event => { 133 | if (!mainWindow) { 134 | // eslint-disable-next-line 135 | console.error('"mainWindow" is not defined'); 136 | return; 137 | } 138 | 139 | mainWindow.setAlwaysOnTop(false); 140 | mainWindow.setMinimumSize(defaultWidth, defaultHeight); 141 | mainWindow.setSize(defaultWidth, defaultHeight); 142 | mainWindow.center(); 143 | mainWindow.resizable = true; 144 | mainWindow.minimizable = true; 145 | mainWindow.maximizable = true; 146 | if (typeof mainWindow.setWindowButtonVisibility === 'function') { 147 | mainWindow.setWindowButtonVisibility(true); 148 | } 149 | mainWindow.setTitle('MyClassroom'); 150 | 151 | event.reply('chime-disable-screen-share-mode-ack'); 152 | }); 153 | }; 154 | 155 | /** 156 | * Add event listeners... 157 | */ 158 | 159 | app.on('window-all-closed', () => { 160 | // Respect the OSX convention of having the application in memory even 161 | // after all windows have been closed 162 | if (process.platform !== 'darwin') { 163 | app.quit(); 164 | } 165 | }); 166 | 167 | app.on('ready', createWindow); 168 | 169 | app.on('activate', () => { 170 | // On macOS it's common to re-create a window in the app when the 171 | // dock icon is clicked and there are no other windows open. 172 | if (mainWindow === null) createWindow(); 173 | }); 174 | -------------------------------------------------------------------------------- /app/main.prod.js.LICENSE: -------------------------------------------------------------------------------- 1 | /*! http://mths.be/fromcodepoint v0.1.0 by @mathias */ 2 | -------------------------------------------------------------------------------- /app/main.prod.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! http://mths.be/fromcodepoint v0.1.0 by @mathias */ 2 | -------------------------------------------------------------------------------- /app/menu.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // MenuItemConstructorOptions is missing "selector" and a few other properties the electron-boilderplate project is using. 3 | 4 | /* eslint @typescript-eslint/ban-ts-ignore: off */ 5 | import { 6 | app, 7 | BrowserWindow, 8 | Menu, 9 | MenuItemConstructorOptions, 10 | shell 11 | } from 'electron'; 12 | 13 | export default class MenuBuilder { 14 | mainWindow: BrowserWindow; 15 | 16 | isCpuUsageVisible = false; 17 | 18 | constructor(mainWindow: BrowserWindow) { 19 | this.mainWindow = mainWindow; 20 | } 21 | 22 | buildMenu() { 23 | if ( 24 | process.env.NODE_ENV === 'development' || 25 | process.env.DEBUG_PROD === 'true' 26 | ) { 27 | this.setupDevelopmentEnvironment(); 28 | } 29 | 30 | const template = 31 | process.platform === 'darwin' 32 | ? this.buildDarwinTemplate() 33 | : this.buildDefaultTemplate(); 34 | 35 | const menu = Menu.buildFromTemplate(template); 36 | Menu.setApplicationMenu(menu); 37 | 38 | return menu; 39 | } 40 | 41 | setupDevelopmentEnvironment() { 42 | this.mainWindow.webContents.on('context-menu', (_, props) => { 43 | const { x, y } = props; 44 | 45 | Menu.buildFromTemplate([ 46 | { 47 | label: 'Inspect element', 48 | click: () => { 49 | this.mainWindow.webContents.inspectElement(x, y); 50 | } 51 | } 52 | ]).popup({ window: this.mainWindow }); 53 | }); 54 | } 55 | 56 | buildDarwinTemplate() { 57 | const subMenuAbout: MenuItemConstructorOptions = { 58 | label: 'Electron', 59 | submenu: [ 60 | { 61 | label: 'About ElectronReact', 62 | // @ts-ignore 63 | selector: 'orderFrontStandardAboutPanel:' 64 | }, 65 | { type: 'separator' }, 66 | { label: 'Services', submenu: [] }, 67 | { type: 'separator' }, 68 | { 69 | label: 'Hide ElectronReact', 70 | accelerator: 'Command+H', 71 | selector: 'hide:' 72 | }, 73 | { 74 | label: 'Hide Others', 75 | accelerator: 'Command+Shift+H', 76 | selector: 'hideOtherApplications:' 77 | }, 78 | { label: 'Show All', selector: 'unhideAllApplications:' }, 79 | { type: 'separator' }, 80 | { 81 | label: 'Quit', 82 | accelerator: 'Command+Q', 83 | click: () => { 84 | app.quit(); 85 | } 86 | } 87 | ] 88 | }; 89 | const subMenuEdit: MenuItemConstructorOptions = { 90 | label: 'Edit', 91 | submenu: [ 92 | // @ts-ignore 93 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, 94 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, 95 | { type: 'separator' }, 96 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, 97 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, 98 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, 99 | { 100 | label: 'Select All', 101 | accelerator: 'Command+A', 102 | selector: 'selectAll:' 103 | } 104 | ] 105 | }; 106 | const subMenuViewDev: MenuItemConstructorOptions = { 107 | label: 'View', 108 | submenu: [ 109 | { 110 | label: 'Reload', 111 | accelerator: 'Command+R', 112 | click: () => { 113 | this.mainWindow.webContents.reload(); 114 | } 115 | }, 116 | { 117 | label: 'Toggle Full Screen', 118 | accelerator: 'Ctrl+Command+F', 119 | click: () => { 120 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 121 | } 122 | }, 123 | { 124 | label: 'Toggle Developer Tools', 125 | accelerator: 'Alt+Command+I', 126 | click: () => { 127 | this.mainWindow.webContents.toggleDevTools(); 128 | } 129 | }, 130 | { 131 | label: 'Toggle CPU Usage', 132 | accelerator: 'Alt+Command+C', 133 | click: () => { 134 | this.mainWindow.webContents.send( 135 | 'chime-toggle-cpu-usage', 136 | !this.isCpuUsageVisible 137 | ); 138 | this.isCpuUsageVisible = !this.isCpuUsageVisible; 139 | } 140 | } 141 | ] 142 | }; 143 | const subMenuViewProd: MenuItemConstructorOptions = { 144 | label: 'View', 145 | submenu: [ 146 | { 147 | label: 'Toggle Developer Tools', 148 | accelerator: 'Alt+Command+I', 149 | click: () => { 150 | this.mainWindow.webContents.toggleDevTools(); 151 | } 152 | }, 153 | { 154 | label: 'Toggle CPU Usage', 155 | accelerator: 'Alt+Command+C', 156 | click: () => { 157 | this.mainWindow.webContents.send( 158 | 'chime-toggle-cpu-usage', 159 | !this.isCpuUsageVisible 160 | ); 161 | this.isCpuUsageVisible = !this.isCpuUsageVisible; 162 | } 163 | } 164 | ] 165 | }; 166 | const subMenuWindow: MenuItemConstructorOptions = { 167 | label: 'Window', 168 | submenu: [ 169 | { 170 | label: 'Minimize', 171 | accelerator: 'Command+M', 172 | // @ts-ignore 173 | selector: 'performMiniaturize:' 174 | }, 175 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, 176 | { type: 'separator' }, 177 | { label: 'Bring All to Front', selector: 'arrangeInFront:' } 178 | ] 179 | }; 180 | const subMenuHelp: MenuItemConstructorOptions = { 181 | label: 'Help', 182 | submenu: [ 183 | { 184 | label: 'Learn More', 185 | click() { 186 | shell.openExternal('https://electronjs.org'); 187 | } 188 | }, 189 | { 190 | label: 'Documentation', 191 | click() { 192 | shell.openExternal( 193 | 'https://github.com/electron/electron/tree/master/docs#readme' 194 | ); 195 | } 196 | }, 197 | { 198 | label: 'Community Discussions', 199 | click() { 200 | shell.openExternal('https://www.electronjs.org/community'); 201 | } 202 | }, 203 | { 204 | label: 'Search Issues', 205 | click() { 206 | shell.openExternal('https://github.com/electron/electron/issues'); 207 | } 208 | } 209 | ] 210 | }; 211 | 212 | const subMenuView = 213 | process.env.NODE_ENV === 'development' || 214 | process.env.DEBUG_PROD === 'true' 215 | ? subMenuViewDev 216 | : subMenuViewProd; 217 | 218 | return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]; 219 | } 220 | 221 | buildDefaultTemplate() { 222 | const templateDefault = [ 223 | { 224 | label: '&File', 225 | submenu: [ 226 | { 227 | label: '&Open', 228 | accelerator: 'Ctrl+O' 229 | }, 230 | { 231 | label: '&Close', 232 | accelerator: 'Ctrl+W', 233 | click: () => { 234 | this.mainWindow.close(); 235 | } 236 | } 237 | ] 238 | }, 239 | { 240 | label: '&View', 241 | submenu: 242 | process.env.NODE_ENV === 'development' || 243 | process.env.DEBUG_PROD === 'true' 244 | ? [ 245 | { 246 | label: '&Reload', 247 | accelerator: 'Ctrl+R', 248 | click: () => { 249 | this.mainWindow.webContents.reload(); 250 | } 251 | }, 252 | { 253 | label: 'Toggle &Full Screen', 254 | accelerator: 'F11', 255 | click: () => { 256 | this.mainWindow.setFullScreen( 257 | !this.mainWindow.isFullScreen() 258 | ); 259 | } 260 | }, 261 | { 262 | label: 'Toggle &Developer Tools', 263 | accelerator: 'Alt+Ctrl+I', 264 | click: () => { 265 | this.mainWindow.webContents.toggleDevTools(); 266 | } 267 | } 268 | ] 269 | : [ 270 | { 271 | label: 'Toggle &Full Screen', 272 | accelerator: 'F11', 273 | click: () => { 274 | this.mainWindow.setFullScreen( 275 | !this.mainWindow.isFullScreen() 276 | ); 277 | } 278 | } 279 | ] 280 | }, 281 | { 282 | label: 'Help', 283 | submenu: [ 284 | { 285 | label: 'Learn More', 286 | click() { 287 | shell.openExternal('https://electronjs.org'); 288 | } 289 | }, 290 | { 291 | label: 'Documentation', 292 | click() { 293 | shell.openExternal( 294 | 'https://github.com/electron/electron/tree/master/docs#readme' 295 | ); 296 | } 297 | }, 298 | { 299 | label: 'Community Discussions', 300 | click() { 301 | shell.openExternal('https://www.electronjs.org/community'); 302 | } 303 | }, 304 | { 305 | label: 'Search Issues', 306 | click() { 307 | shell.openExternal('https://github.com/electron/electron/issues'); 308 | } 309 | } 310 | ] 311 | } 312 | ]; 313 | 314 | return templateDefault; 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-chime-sdk-classroom-demo", 3 | "productName": "MyClassroom", 4 | "version": "1.0.0", 5 | "description": "Demonstrates how to use the Amazon Chime SDK to build an online classroom with Electron and React", 6 | "main": "./main.prod.js", 7 | "author": { 8 | "name": "Amazon Chime SDK Team", 9 | "url": "https://github.com/aws-samples/amazon-chime-sdk-classroom-demo" 10 | }, 11 | "scripts": { 12 | "electron-rebuild": "node -r ../internals/scripts/BabelRegister.js ../internals/scripts/ElectronRebuild.js", 13 | "postinstall": "yarn electron-rebuild" 14 | }, 15 | "license": "Apache-2.0", 16 | "dependencies": {} 17 | } 18 | -------------------------------------------------------------------------------- /app/providers/ChimeProvider.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React, { ReactNode } from 'react'; 5 | 6 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 7 | import getChimeContext from '../context/getChimeContext'; 8 | 9 | type Props = { 10 | children: ReactNode; 11 | }; 12 | 13 | export default function ChimeProvider(props: Props) { 14 | const { children } = props; 15 | const chimeSdkWrapper = new ChimeSdkWrapper(); 16 | const ChimeContext = getChimeContext(); 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/providers/I18nProvider.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React, { ReactNode } from 'react'; 5 | import { IntlProvider } from 'react-intl'; 6 | 7 | import enUS from '../i18n/en-US'; 8 | 9 | const DEFAULT_LOCALE = 'en-US'; 10 | 11 | const messages = { 12 | [DEFAULT_LOCALE]: enUS 13 | }; 14 | 15 | type Props = { 16 | children: ReactNode; 17 | }; 18 | 19 | export default function I18nProvider(props: Props) { 20 | const { children } = props; 21 | return ( 22 | 29 | {children} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/providers/MeetingStatusProvider.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { 5 | MeetingSessionStatus, 6 | MeetingSessionStatusCode 7 | } from 'amazon-chime-sdk-js'; 8 | import React, { 9 | ReactNode, 10 | useContext, 11 | useEffect, 12 | useRef, 13 | useState 14 | } from 'react'; 15 | import { useHistory, useLocation } from 'react-router-dom'; 16 | 17 | import ChimeSdkWrapper from '../chime/ChimeSdkWrapper'; 18 | import getChimeContext from '../context/getChimeContext'; 19 | import getMeetingStatusContext from '../context/getMeetingStatusContext'; 20 | import getUIStateContext from '../context/getUIStateContext'; 21 | import ClassMode from '../enums/ClassMode'; 22 | import MeetingStatus from '../enums/MeetingStatus'; 23 | 24 | type Props = { 25 | children: ReactNode; 26 | }; 27 | 28 | export default function MeetingStatusProvider(props: Props) { 29 | const MeetingStatusContext = getMeetingStatusContext(); 30 | const { children } = props; 31 | const chime: ChimeSdkWrapper | null = useContext(getChimeContext()); 32 | const [meetingStatus, setMeetingStatus] = useState<{ 33 | meetingStatus: MeetingStatus; 34 | errorMessage?: string; 35 | }>({ 36 | meetingStatus: MeetingStatus.Loading 37 | }); 38 | const [state] = useContext(getUIStateContext()); 39 | const history = useHistory(); 40 | const query = new URLSearchParams(useLocation().search); 41 | const audioElement = useRef(null); 42 | 43 | useEffect(() => { 44 | const start = async () => { 45 | try { 46 | await chime?.createRoom( 47 | query.get('title'), 48 | query.get('name'), 49 | query.get('region'), 50 | state.classMode === ClassMode.Student ? 'student' : 'teacher', 51 | query.get('optionalFeature') 52 | ); 53 | 54 | setMeetingStatus({ 55 | meetingStatus: MeetingStatus.Succeeded 56 | }); 57 | 58 | chime?.audioVideo?.addObserver({ 59 | audioVideoDidStop: (sessionStatus: MeetingSessionStatus): void => { 60 | if ( 61 | sessionStatus.statusCode() === 62 | MeetingSessionStatusCode.AudioCallEnded 63 | ) { 64 | history.push('/'); 65 | chime?.leaveRoom(state.classMode === ClassMode.Teacher); 66 | } 67 | } 68 | }); 69 | 70 | await chime?.joinRoom(audioElement.current); 71 | } catch (error) { 72 | // eslint-disable-next-line 73 | console.error(error); 74 | setMeetingStatus({ 75 | meetingStatus: MeetingStatus.Failed, 76 | errorMessage: error.message 77 | }); 78 | } 79 | }; 80 | start(); 81 | }, []); 82 | 83 | return ( 84 | 85 | {/* eslint-disable-next-line */} 86 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /app/providers/UIStateProvider.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React, { ReactNode, useReducer } from 'react'; 5 | 6 | import getUIStateContext, { 7 | initialState, 8 | SetClassModeActon, 9 | StateType 10 | } from '../context/getUIStateContext'; 11 | 12 | const reducer = (state: StateType, action: SetClassModeActon): StateType => { 13 | switch (action.type) { 14 | case 'SET_CLASS_MODE': 15 | return { 16 | ...state, 17 | classMode: action.payload.classMode 18 | }; 19 | default: 20 | throw new Error(); 21 | } 22 | }; 23 | 24 | type Props = { 25 | children: ReactNode; 26 | }; 27 | 28 | export default function UIStateProvider(props: Props) { 29 | const { children } = props; 30 | const UIStateContext = getUIStateContext(); 31 | return ( 32 | 33 | {children} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/types/DeviceType.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Option } from 'react-dropdown'; 5 | 6 | type DeviceType = Option; 7 | 8 | export default DeviceType; 9 | -------------------------------------------------------------------------------- /app/types/FullDeviceInfoType.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import DeviceType from './DeviceType'; 5 | 6 | type FullDeviceInfoType = { 7 | currentAudioInputDevice: DeviceType | null; 8 | currentAudioOutputDevice: DeviceType | null; 9 | currentVideoInputDevice: DeviceType | null; 10 | audioInputDevices: DeviceType[]; 11 | audioOutputDevices: DeviceType[]; 12 | videoInputDevices: DeviceType[]; 13 | }; 14 | 15 | export default FullDeviceInfoType; 16 | -------------------------------------------------------------------------------- /app/types/MessageUpdateCallbackType.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { DataMessage } from 'amazon-chime-sdk-js'; 5 | 6 | type MessageUpdateCallbackType = { 7 | topic: string; 8 | callback: (message: DataMessage) => void; 9 | }; 10 | 11 | export default MessageUpdateCallbackType; 12 | -------------------------------------------------------------------------------- /app/types/RegionType.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Option } from 'react-dropdown'; 5 | 6 | type RegionType = Option; 7 | 8 | export default RegionType; 9 | -------------------------------------------------------------------------------- /app/types/RosterAttendeeType.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | type RosterAttendeeType = { 5 | name?: string; 6 | muted?: boolean; 7 | signalStrength?: number; 8 | volume?: number; 9 | active?: boolean; 10 | }; 11 | 12 | export default RosterAttendeeType; 13 | -------------------------------------------------------------------------------- /app/types/RosterType.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import RosterAttendeeType from './RosterAttendeeType'; 5 | 6 | type RosterType = { 7 | [attendeeId: string]: RosterAttendeeType; 8 | }; 9 | 10 | export default RosterType; 11 | -------------------------------------------------------------------------------- /app/utils/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/app/utils/.gitkeep -------------------------------------------------------------------------------- /app/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, import/no-extraneous-dependencies: off */ 2 | 3 | const developmentEnvironments = ['development', 'test']; 4 | 5 | const developmentPlugins = [require('react-hot-loader/babel')]; 6 | 7 | const productionPlugins = [ 8 | require('babel-plugin-dev-expression'), 9 | 10 | // babel-preset-react-optimize 11 | require('@babel/plugin-transform-react-constant-elements'), 12 | require('@babel/plugin-transform-react-inline-elements'), 13 | require('babel-plugin-transform-react-remove-prop-types') 14 | ]; 15 | 16 | module.exports = api => { 17 | // See docs about api at https://babeljs.io/docs/en/config-files#apicache 18 | 19 | const development = api.env(developmentEnvironments); 20 | 21 | return { 22 | presets: [ 23 | // @babel/preset-env will automatically target our browserslist targets 24 | require('@babel/preset-env'), 25 | require('@babel/preset-typescript'), 26 | [require('@babel/preset-react'), { development }] 27 | ], 28 | plugins: [ 29 | // Stage 0 30 | require('@babel/plugin-proposal-function-bind'), 31 | 32 | // Stage 1 33 | require('@babel/plugin-proposal-export-default-from'), 34 | require('@babel/plugin-proposal-logical-assignment-operators'), 35 | [require('@babel/plugin-proposal-optional-chaining'), { loose: false }], 36 | [ 37 | require('@babel/plugin-proposal-pipeline-operator'), 38 | { proposal: 'minimal' } 39 | ], 40 | [ 41 | require('@babel/plugin-proposal-nullish-coalescing-operator'), 42 | { loose: false } 43 | ], 44 | require('@babel/plugin-proposal-do-expressions'), 45 | 46 | // Stage 2 47 | [require('@babel/plugin-proposal-decorators'), { legacy: true }], 48 | require('@babel/plugin-proposal-function-sent'), 49 | require('@babel/plugin-proposal-export-namespace-from'), 50 | require('@babel/plugin-proposal-numeric-separator'), 51 | require('@babel/plugin-proposal-throw-expressions'), 52 | 53 | // Stage 3 54 | require('@babel/plugin-syntax-dynamic-import'), 55 | require('@babel/plugin-syntax-import-meta'), 56 | [require('@babel/plugin-proposal-class-properties'), { loose: true }], 57 | [require('@babel/plugin-proposal-private-methods'), { loose: true }], 58 | require('@babel/plugin-proposal-json-strings'), 59 | 60 | ...(development ? developmentPlugins : productionPlugins) 61 | ] 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /configs/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | 8 | import { dependencies as externals } from '../app/package.json'; 9 | 10 | export default { 11 | externals: [...Object.keys(externals || {})], 12 | 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.tsx?$/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | cacheDirectory: true 22 | } 23 | } 24 | }, 25 | { 26 | test: /\.m?js/, 27 | resolve: { 28 | fullySpecified: false 29 | } 30 | } 31 | ] 32 | }, 33 | 34 | output: { 35 | path: path.join(__dirname, '..', 'app'), 36 | // https://github.com/webpack/webpack/issues/1114 37 | libraryTarget: 'commonjs2' 38 | }, 39 | 40 | /** 41 | * Determine the array of extensions that should be used to resolve modules. 42 | */ 43 | resolve: { 44 | extensions: ['.wasm', '.mjs', '.js', '.jsx', '.json', '.ts', '.tsx'], 45 | modules: [path.join(__dirname, '..', 'app'), 'node_modules'] 46 | }, 47 | 48 | optimization: { 49 | moduleIds: 'named' 50 | }, 51 | 52 | plugins: [ 53 | new webpack.EnvironmentPlugin({ 54 | NODE_ENV: 'production' 55 | }) 56 | ] 57 | }; 58 | -------------------------------------------------------------------------------- /configs/webpack.config.eslint.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | require('@babel/register'); 3 | 4 | module.exports = require('./webpack.config.renderer.dev.babel').default; 5 | -------------------------------------------------------------------------------- /configs/webpack.config.main.prod.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import TerserPlugin from 'terser-webpack-plugin'; 7 | import webpack from 'webpack'; 8 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 9 | import merge from 'webpack-merge'; 10 | 11 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 12 | import DeleteSourceMaps from '../internals/scripts/DeleteSourceMaps'; 13 | import baseConfig from './webpack.config.base'; 14 | 15 | CheckNodeEnv('production'); 16 | DeleteSourceMaps(); 17 | 18 | const devtoolsConfig = 19 | process.env.DEBUG_PROD === 'true' 20 | ? { 21 | devtool: 'source-map' 22 | } 23 | : {}; 24 | 25 | export default merge.smart(baseConfig, { 26 | ...devtoolsConfig, 27 | 28 | mode: 'production', 29 | 30 | target: 'electron-main', 31 | 32 | entry: './app/main.dev.ts', 33 | 34 | output: { 35 | path: path.join(__dirname, '..'), 36 | filename: './app/main.prod.js' 37 | }, 38 | 39 | optimization: { 40 | minimizer: process.env.E2E_BUILD 41 | ? [] 42 | : [ 43 | new TerserPlugin({ 44 | parallel: true 45 | }) 46 | ] 47 | }, 48 | 49 | plugins: [ 50 | new BundleAnalyzerPlugin({ 51 | analyzerMode: 52 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 53 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 54 | }), 55 | 56 | /** 57 | * Create global constants which can be configured at compile time. 58 | * 59 | * Useful for allowing different behaviour between development builds and 60 | * release builds 61 | * 62 | * NODE_ENV should be production so that modules do not perform certain 63 | * development checks 64 | */ 65 | new webpack.EnvironmentPlugin({ 66 | NODE_ENV: 'production', 67 | DEBUG_PROD: false, 68 | START_MINIMIZED: false, 69 | E2E_BUILD: false 70 | }) 71 | ], 72 | 73 | /** 74 | * Disables webpack processing of __dirname and __filename. 75 | * If you run the bundle in node.js it falls back to these values of node.js. 76 | * https://github.com/webpack/webpack/issues/2010 77 | */ 78 | node: { 79 | __dirname: false, 80 | __filename: false 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.dev.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for development electron renderer process that uses 3 | * Hot-Module-Replacement 4 | * 5 | * https://webpack.js.org/concepts/hot-module-replacement/ 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import { execSync, spawn } from 'child_process'; 10 | import fs from 'fs'; 11 | import path from 'path'; 12 | import webpack from 'webpack'; 13 | import merge from 'webpack-merge'; 14 | 15 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 16 | import baseConfig from './webpack.config.base'; 17 | 18 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 19 | // at the dev webpack config is not accidentally run in a production environment 20 | if (process.env.NODE_ENV === 'production') { 21 | CheckNodeEnv('development'); 22 | } 23 | 24 | const port = process.env.PORT || 1212; 25 | const publicPath = `http://localhost:${port}/dist`; 26 | const dll = path.join(__dirname, '..', 'dll'); 27 | const manifest = path.resolve(dll, 'renderer.json'); 28 | const requiredByDLLConfig = module.parent.filename.includes( 29 | 'webpack.config.renderer.dev.dll' 30 | ); 31 | 32 | /** 33 | * Warn if the DLL is not built 34 | */ 35 | if (!requiredByDLLConfig && !(fs.existsSync(dll) && fs.existsSync(manifest))) { 36 | console.log( 37 | chalk.black.bgYellow.bold( 38 | 'The DLL files are missing. Sit back while we build them for you with "yarn build-dll"' 39 | ) 40 | ); 41 | execSync('yarn build-dll'); 42 | } 43 | 44 | export default merge.smart(baseConfig, { 45 | devtool: 'inline-source-map', 46 | 47 | mode: 'development', 48 | 49 | target: 'electron-renderer', 50 | 51 | entry: [ 52 | ...(process.env.PLAIN_HMR ? [] : ['react-hot-loader/patch']), 53 | `webpack-dev-server/client?http://localhost:${port}/`, 54 | 'webpack/hot/only-dev-server', 55 | require.resolve('../app/index.tsx') 56 | ], 57 | 58 | output: { 59 | publicPath: `http://localhost:${port}/dist/`, 60 | filename: 'renderer.dev.js' 61 | }, 62 | 63 | module: { 64 | rules: [ 65 | { 66 | test: /\.global\.css$/, 67 | use: [ 68 | { 69 | loader: 'style-loader' 70 | }, 71 | { 72 | loader: 'css-loader', 73 | options: { 74 | sourceMap: true 75 | } 76 | } 77 | ] 78 | }, 79 | { 80 | test: /^((?!\.global).)*\.css$/, 81 | use: [ 82 | { 83 | loader: 'style-loader' 84 | }, 85 | { 86 | loader: 'css-loader', 87 | options: { 88 | modules: { 89 | localIdentName: '[name]__[local]__[hash:base64:5]' 90 | }, 91 | sourceMap: true, 92 | importLoaders: 1 93 | } 94 | } 95 | ] 96 | }, 97 | // SASS support - compile all .global.scss files and pipe it to style.css 98 | { 99 | test: /\.global\.(scss|sass)$/, 100 | use: [ 101 | { 102 | loader: 'style-loader' 103 | }, 104 | { 105 | loader: 'css-loader', 106 | options: { 107 | sourceMap: true 108 | } 109 | }, 110 | { 111 | loader: 'sass-loader' 112 | } 113 | ] 114 | }, 115 | // SASS support - compile all other .scss files and pipe it to style.css 116 | { 117 | test: /^((?!\.global).)*\.(scss|sass)$/, 118 | use: [ 119 | { 120 | loader: 'style-loader' 121 | }, 122 | { 123 | loader: '@teamsupercell/typings-for-css-modules-loader' 124 | }, 125 | { 126 | loader: 'css-loader', 127 | options: { 128 | modules: { 129 | localIdentName: '[name]__[local]__[hash:base64:5]' 130 | }, 131 | sourceMap: true, 132 | importLoaders: 1 133 | } 134 | }, 135 | { 136 | loader: 'sass-loader' 137 | } 138 | ] 139 | }, 140 | // WOFF Font 141 | { 142 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 143 | use: { 144 | loader: 'url-loader', 145 | options: { 146 | limit: 10000, 147 | mimetype: 'application/font-woff' 148 | } 149 | } 150 | }, 151 | // WOFF2 Font 152 | { 153 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 154 | use: { 155 | loader: 'url-loader', 156 | options: { 157 | limit: 10000, 158 | mimetype: 'application/font-woff' 159 | } 160 | } 161 | }, 162 | // TTF Font 163 | { 164 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 165 | use: { 166 | loader: 'url-loader', 167 | options: { 168 | limit: 10000, 169 | mimetype: 'application/octet-stream' 170 | } 171 | } 172 | }, 173 | // EOT Font 174 | { 175 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 176 | use: 'file-loader' 177 | }, 178 | // SVG Font 179 | { 180 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 181 | use: { 182 | loader: 'url-loader', 183 | options: { 184 | limit: 10000, 185 | mimetype: 'image/svg+xml' 186 | } 187 | } 188 | }, 189 | // Common Image Formats 190 | { 191 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 192 | use: 'url-loader' 193 | } 194 | ] 195 | }, 196 | resolve: { 197 | alias: { 198 | 'react-dom': '@hot-loader/react-dom' 199 | } 200 | }, 201 | plugins: [ 202 | requiredByDLLConfig 203 | ? null 204 | : new webpack.DllReferencePlugin({ 205 | context: path.join(__dirname, '..', 'dll'), 206 | manifest: require(manifest), 207 | sourceType: 'var' 208 | }), 209 | new webpack.NoEmitOnErrorsPlugin(), 210 | 211 | /** 212 | * Create global constants which can be configured at compile time. 213 | * 214 | * Useful for allowing different behaviour between development builds and 215 | * release builds 216 | * 217 | * NODE_ENV should be production so that modules do not perform certain 218 | * development checks 219 | * 220 | * By default, use 'development' as NODE_ENV. This can be overriden with 221 | * 'staging', for example, by changing the ENV variables in the npm scripts 222 | */ 223 | new webpack.EnvironmentPlugin({ 224 | NODE_ENV: 'development' 225 | }), 226 | 227 | new webpack.LoaderOptionsPlugin({ 228 | debug: true 229 | }) 230 | ], 231 | 232 | node: { 233 | __dirname: false, 234 | __filename: false 235 | }, 236 | 237 | devServer: { 238 | port, 239 | hot: true, 240 | headers: { 'Access-Control-Allow-Origin': '*' }, 241 | static: { 242 | directory: path.join(__dirname, 'dist'), 243 | watch: { 244 | aggregateTimeout: 300, 245 | ignored: /node_modules/, 246 | poll: 100 247 | } 248 | }, 249 | historyApiFallback: { 250 | verbose: true, 251 | disableDotRule: false 252 | }, 253 | devMiddleware: { 254 | publicPath, 255 | stats: 'errors-only' 256 | }, 257 | onBeforeSetupMiddleware() { 258 | if (process.env.START_HOT) { 259 | console.log('Starting Main Process...'); 260 | spawn('npm', ['run', 'start-main-dev'], { 261 | shell: true, 262 | env: process.env, 263 | stdio: 'inherit' 264 | }) 265 | .on('close', code => process.exit(code)) 266 | .on('error', spawnError => console.error(spawnError)); 267 | } 268 | } 269 | } 270 | }); 271 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.dev.dll.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import merge from 'webpack-merge'; 8 | 9 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 10 | import { dependencies } from '../package.json'; 11 | import baseConfig from './webpack.config.base'; 12 | 13 | CheckNodeEnv('development'); 14 | 15 | const dist = path.join(__dirname, '..', 'dll'); 16 | 17 | export default merge.smart(baseConfig, { 18 | context: path.join(__dirname, '..'), 19 | 20 | devtool: 'eval', 21 | 22 | mode: 'development', 23 | 24 | target: 'electron-renderer', 25 | 26 | externals: ['fsevents', 'crypto-browserify'], 27 | 28 | /** 29 | * Use `module` from `webpack.config.renderer.dev.js` 30 | */ 31 | module: require('./webpack.config.renderer.dev.babel').default.module, 32 | 33 | entry: { 34 | renderer: Object.keys(dependencies || {}) 35 | }, 36 | 37 | output: { 38 | library: 'renderer', 39 | path: dist, 40 | filename: '[name].dev.dll.js', 41 | libraryTarget: 'var' 42 | }, 43 | 44 | plugins: [ 45 | new webpack.DllPlugin({ 46 | path: path.join(dist, '[name].json'), 47 | name: '[name]' 48 | }), 49 | 50 | /** 51 | * Create global constants which can be configured at compile time. 52 | * 53 | * Useful for allowing different behaviour between development builds and 54 | * release builds 55 | * 56 | * NODE_ENV should be production so that modules do not perform certain 57 | * development checks 58 | */ 59 | new webpack.EnvironmentPlugin({ 60 | NODE_ENV: 'development' 61 | }), 62 | 63 | new webpack.LoaderOptionsPlugin({ 64 | debug: true, 65 | options: { 66 | context: path.join(__dirname, '..', 'app'), 67 | output: { 68 | path: path.join(__dirname, '..', 'dll') 69 | } 70 | } 71 | }) 72 | ] 73 | }); 74 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.prod.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 6 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; 7 | import path from 'path'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import webpack from 'webpack'; 10 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 11 | import merge from 'webpack-merge'; 12 | 13 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 14 | import DeleteSourceMaps from '../internals/scripts/DeleteSourceMaps'; 15 | import baseConfig from './webpack.config.base'; 16 | 17 | CheckNodeEnv('production'); 18 | DeleteSourceMaps(); 19 | 20 | const devtoolsConfig = 21 | process.env.DEBUG_PROD === 'true' 22 | ? { 23 | devtool: 'source-map' 24 | } 25 | : {}; 26 | 27 | export default merge.smart(baseConfig, { 28 | ...devtoolsConfig, 29 | 30 | mode: 'production', 31 | 32 | target: 'electron-preload', 33 | 34 | entry: path.join(__dirname, '..', 'app/index.tsx'), 35 | 36 | output: { 37 | path: path.join(__dirname, '..', 'app/dist'), 38 | publicPath: './dist/', 39 | filename: 'renderer.prod.js' 40 | }, 41 | 42 | module: { 43 | rules: [ 44 | // Extract all .global.css to style.css as is 45 | { 46 | test: /\.global\.css$/, 47 | use: [ 48 | { 49 | loader: MiniCssExtractPlugin.loader, 50 | options: { 51 | publicPath: './' 52 | } 53 | }, 54 | { 55 | loader: 'css-loader', 56 | options: { 57 | sourceMap: true 58 | } 59 | } 60 | ] 61 | }, 62 | // Pipe other styles through css modules and append to style.css 63 | { 64 | test: /^((?!\.global).)*\.css$/, 65 | use: [ 66 | { 67 | loader: MiniCssExtractPlugin.loader 68 | }, 69 | { 70 | loader: 'css-loader', 71 | options: { 72 | modules: { 73 | localIdentName: '[name]__[local]__[hash:base64:5]' 74 | }, 75 | sourceMap: true 76 | } 77 | } 78 | ] 79 | }, 80 | // Add SASS support - compile all .global.scss files and pipe it to style.css 81 | { 82 | test: /\.global\.(scss|sass)$/, 83 | use: [ 84 | { 85 | loader: MiniCssExtractPlugin.loader 86 | }, 87 | { 88 | loader: 'css-loader', 89 | options: { 90 | sourceMap: true, 91 | importLoaders: 1 92 | } 93 | }, 94 | { 95 | loader: 'sass-loader', 96 | options: { 97 | sourceMap: true 98 | } 99 | } 100 | ] 101 | }, 102 | // Add SASS support - compile all other .scss files and pipe it to style.css 103 | { 104 | test: /^((?!\.global).)*\.(scss|sass)$/, 105 | use: [ 106 | { 107 | loader: MiniCssExtractPlugin.loader 108 | }, 109 | { 110 | loader: 'css-loader', 111 | options: { 112 | modules: { 113 | localIdentName: '[name]__[local]__[hash:base64:5]' 114 | }, 115 | importLoaders: 1, 116 | sourceMap: true 117 | } 118 | }, 119 | { 120 | loader: 'sass-loader', 121 | options: { 122 | sourceMap: true 123 | } 124 | } 125 | ] 126 | }, 127 | // WOFF Font 128 | { 129 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 130 | use: { 131 | loader: 'url-loader', 132 | options: { 133 | limit: 10000, 134 | mimetype: 'application/font-woff' 135 | } 136 | } 137 | }, 138 | // WOFF2 Font 139 | { 140 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 141 | use: { 142 | loader: 'url-loader', 143 | options: { 144 | limit: 10000, 145 | mimetype: 'application/font-woff' 146 | } 147 | } 148 | }, 149 | // TTF Font 150 | { 151 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 152 | use: { 153 | loader: 'url-loader', 154 | options: { 155 | limit: 10000, 156 | mimetype: 'application/octet-stream' 157 | } 158 | } 159 | }, 160 | // EOT Font 161 | { 162 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 163 | use: 'file-loader' 164 | }, 165 | // SVG Font 166 | { 167 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 168 | use: { 169 | loader: 'url-loader', 170 | options: { 171 | limit: 10000, 172 | mimetype: 'image/svg+xml' 173 | } 174 | } 175 | }, 176 | // Common Image Formats 177 | { 178 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 179 | use: 'url-loader' 180 | } 181 | ] 182 | }, 183 | 184 | optimization: { 185 | minimizer: process.env.E2E_BUILD 186 | ? [] 187 | : [ 188 | new TerserPlugin({ 189 | parallel: true 190 | }), 191 | new CssMinimizerPlugin() 192 | ] 193 | }, 194 | 195 | plugins: [ 196 | /** 197 | * Create global constants which can be configured at compile time. 198 | * 199 | * Useful for allowing different behaviour between development builds and 200 | * release builds 201 | * 202 | * NODE_ENV should be production so that modules do not perform certain 203 | * development checks 204 | */ 205 | new webpack.EnvironmentPlugin({ 206 | NODE_ENV: 'production', 207 | DEBUG_PROD: false, 208 | E2E_BUILD: false 209 | }), 210 | 211 | new MiniCssExtractPlugin({ 212 | filename: 'style.css' 213 | }), 214 | 215 | new BundleAnalyzerPlugin({ 216 | analyzerMode: 217 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 218 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 219 | }) 220 | ] 221 | }); 222 | -------------------------------------------------------------------------------- /internals/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /internals/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /internals/scripts/BabelRegister.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | require('@babel/register')({ 4 | extensions: ['.es6', '.es', '.jsx', '.js', '.mjs', '.ts', '.tsx'], 5 | cwd: path.join(__dirname, '..', '..') 6 | }); 7 | -------------------------------------------------------------------------------- /internals/scripts/CheckBuildsExist.js: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import chalk from 'chalk'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js'); 7 | const rendererPath = path.join( 8 | __dirname, 9 | '..', 10 | '..', 11 | 'app', 12 | 'dist', 13 | 'renderer.prod.js' 14 | ); 15 | 16 | if (!fs.existsSync(mainPath)) { 17 | throw new Error( 18 | chalk.whiteBright.bgRed.bold( 19 | 'The main process is not built yet. Build it by running "yarn build-main"' 20 | ) 21 | ); 22 | } 23 | 24 | if (!fs.existsSync(rendererPath)) { 25 | throw new Error( 26 | chalk.whiteBright.bgRed.bold( 27 | 'The renderer process is not built yet. Build it by running "yarn build-renderer"' 28 | ) 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /internals/scripts/CheckNativeDep.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { execSync } from 'child_process'; 3 | import fs from 'fs'; 4 | 5 | import { dependencies } from '../../package.json'; 6 | 7 | if (dependencies) { 8 | const dependenciesKeys = Object.keys(dependencies); 9 | const nativeDeps = fs 10 | .readdirSync('node_modules') 11 | .filter(folder => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 12 | try { 13 | // Find the reason for why the dependency is installed. If it is installed 14 | // because of a devDependency then that is okay. Warn when it is installed 15 | // because of a dependency 16 | const { dependencies: dependenciesObject } = JSON.parse( 17 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 18 | ); 19 | const rootDependencies = Object.keys(dependenciesObject); 20 | const filteredRootDependencies = rootDependencies.filter(rootDependency => 21 | dependenciesKeys.includes(rootDependency) 22 | ); 23 | if (filteredRootDependencies.length > 0) { 24 | const plural = filteredRootDependencies.length > 1; 25 | console.log(` 26 | ${chalk.whiteBright.bgYellow.bold( 27 | 'Webpack does not work with native dependencies.' 28 | )} 29 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 30 | plural ? 'are native dependencies' : 'is a native dependency' 31 | } and should be installed inside of the "./app" folder. 32 | First uninstall the packages from "./package.json": 33 | ${chalk.whiteBright.bgGreen.bold('yarn remove your-package')} 34 | ${chalk.bold( 35 | 'Then, instead of installing the package to the root "./package.json":' 36 | )} 37 | ${chalk.whiteBright.bgRed.bold('yarn add your-package')} 38 | ${chalk.bold('Install the package to "./app/package.json"')} 39 | ${chalk.whiteBright.bgGreen.bold('cd ./app && yarn add your-package')} 40 | Read more about native dependencies at: 41 | ${chalk.bold( 42 | 'https://github.com/electron-react-boilerplate/electron-react-boilerplate/wiki/Module-Structure----Two-package.json-Structure' 43 | )} 44 | `); 45 | process.exit(1); 46 | } 47 | } catch (e) { 48 | console.log('Native dependencies could not be checked'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internals/scripts/CheckNodeEnv.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function CheckNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 12 | ) 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internals/scripts/CheckPortInUse.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn dev` 11 | ) 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /internals/scripts/CheckYarn.js: -------------------------------------------------------------------------------- 1 | if (!/yarn\.js$/.test(process.env.npm_execpath || '')) { 2 | console.warn( 3 | "\u001b[33mYou don't seem to be using yarn. This could produce unexpected results.\u001b[39m" 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /internals/scripts/DeleteSourceMaps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rimraf from 'rimraf'; 3 | 4 | export default function deleteSourceMaps() { 5 | rimraf.sync(path.join(__dirname, '../../app/dist/*.js.map')); 6 | rimraf.sync(path.join(__dirname, '../../app/*.js.map')); 7 | } 8 | -------------------------------------------------------------------------------- /internals/scripts/ElectronRebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | import { dependencies } from '../../app/package.json'; 6 | 7 | const nodeModulesPath = path.join(__dirname, '..', '..', 'app', 'node_modules'); 8 | 9 | if ( 10 | Object.keys(dependencies || {}).length > 0 && 11 | fs.existsSync(nodeModulesPath) 12 | ) { 13 | const electronRebuildCmd = 14 | '../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .'; 15 | const cmd = 16 | process.platform === 'win32' 17 | ? electronRebuildCmd.replace(/\//g, '\\') 18 | : electronRebuildCmd; 19 | execSync(cmd, { 20 | cwd: path.join(__dirname, '..', '..', 'app'), 21 | stdio: 'inherit' 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icon.png -------------------------------------------------------------------------------- /resources/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icons/1024x1024.png -------------------------------------------------------------------------------- /resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icons/128x128.png -------------------------------------------------------------------------------- /resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icons/16x16.png -------------------------------------------------------------------------------- /resources/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icons/24x24.png -------------------------------------------------------------------------------- /resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icons/256x256.png -------------------------------------------------------------------------------- /resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icons/32x32.png -------------------------------------------------------------------------------- /resources/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icons/48x48.png -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icons/512x512.png -------------------------------------------------------------------------------- /resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icons/64x64.png -------------------------------------------------------------------------------- /resources/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/icons/96x96.png -------------------------------------------------------------------------------- /resources/readme-hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-classroom-demo/c8356972c0f97387aa7e18193ea33c0e2c592b48/resources/readme-hero.jpg -------------------------------------------------------------------------------- /script/cloud9-resize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | if [ -z "$C9_PROJECT" ] 6 | then 7 | echo "Ignoring - not a Cloud9 environment" 8 | exit 0 9 | fi 10 | 11 | # Size in GiB 12 | SIZE=100 13 | 14 | # Install the jq command-line JSON processor. 15 | sudo yum -y install jq 16 | 17 | # Get the ID of the envrionment host Amazon EC2 instance. 18 | INSTANCEID=$(curl http://169.254.169.254/latest/meta-data//instance-id) 19 | 20 | # Get the ID of the Amazon EBS volume associated with the instance. 21 | VOLUMEID=$(aws ec2 describe-instances --instance-id $INSTANCEID | jq -r .Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId) 22 | 23 | # Skip resizing if already optimizing 24 | if [ "$(aws ec2 describe-volumes-modifications --volume-id $VOLUMEID --filters Name=modification-state,Values="optimizing","completed" | jq '.VolumesModifications | length')" == "1" ]; then 25 | exit 0 26 | fi 27 | 28 | # Resize the EBS volume. 29 | aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE 30 | 31 | # Wait for the resize to finish. 32 | while [ "$(aws ec2 describe-volumes-modifications --volume-id $VOLUMEID --filters Name=modification-state,Values="optimizing","completed" | jq '.VolumesModifications | length')" != "1" ]; do 33 | sleep 1 34 | done 35 | 36 | # Rewrite the partition table so that the partition takes up all the space that it can. 37 | sudo growpart /dev/xvda 1 38 | 39 | # Expand the size of the file system. 40 | sudo resize2fs /dev/xvda1 41 | -------------------------------------------------------------------------------- /script/deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawnSync } = require('child_process'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | // Parameters 8 | let region = ''; 9 | let bucket = ''; 10 | let stack = ''; 11 | let appName = ''; 12 | 13 | function usage() { 14 | console.log( 15 | `Usage: deploy.js -r -a -s -b ` 16 | ); 17 | console.log(` -r, --region Deployment region, required`); 18 | console.log( 19 | ` -a, --app-name Application name (e.g. MyClassroom), required` 20 | ); 21 | console.log( 22 | ` -s, --stack-name CloudFormation stack name (e.g. -myclassroom-1), required` 23 | ); 24 | console.log( 25 | ` -b, --s3-bucket Globally unique S3 bucket prefix for deployment (e.g. -myclassroom-1), required` 26 | ); 27 | console.log(''); 28 | console.log('Optional:'); 29 | console.log(` -h, --help Show help and exit`); 30 | } 31 | 32 | function ensureBucket(bucketName, isWebsite) { 33 | const s3Api = spawnSync('aws', [ 34 | 's3api', 35 | 'head-bucket', 36 | '--bucket', 37 | `${bucketName}`, 38 | '--region', 39 | `${region}` 40 | ]); 41 | if (s3Api.status !== 0) { 42 | console.log(`Creating S3 bucket ${bucketName}`); 43 | const s3 = spawnSync('aws', [ 44 | 's3', 45 | 'mb', 46 | `s3://${bucketName}`, 47 | '--region', 48 | `${region}` 49 | ]); 50 | if (s3.status !== 0) { 51 | console.log(`Failed to create bucket: ${JSON.stringify(s3)}`); 52 | console.log((s3.stderr || s3.stdout).toString()); 53 | process.exit(s3.status); 54 | } 55 | if (isWebsite) { 56 | const s3Website = spawnSync('aws', [ 57 | 's3', 58 | 'website', 59 | `s3://${bucketName}`, 60 | '--index-document', 61 | `index.html`, 62 | '--error-document', 63 | 'error.html' 64 | ]); 65 | if (s3Website.status !== 0) { 66 | console.log(`Failed to create bucket: ${JSON.stringify(s3Website)}`); 67 | console.log((s3Website.stderr || s3Website.stdout).toString()); 68 | process.exit(s3Website.status); 69 | } 70 | } 71 | } 72 | } 73 | 74 | function getArgOrExit(i, args) { 75 | if (i >= args.length) { 76 | console.log('Too few arguments'); 77 | usage(); 78 | process.exit(1); 79 | } 80 | return args[i].trim(); 81 | } 82 | 83 | function parseArgs() { 84 | const args = process.argv.slice(2); 85 | let i = 0; 86 | while (i < args.length) { 87 | switch (args[i]) { 88 | case '-h': 89 | case '--help': 90 | usage(); 91 | process.exit(0); 92 | break; 93 | case '-r': 94 | case '--region': 95 | region = getArgOrExit(++i, args); 96 | break; 97 | case '-b': 98 | case '--s3-bucket': 99 | bucket = getArgOrExit(++i, args); 100 | break; 101 | case '-a': 102 | case '--app-name': 103 | appName = getArgOrExit(++i, args).replace(/[\W_]+/g, ''); 104 | break; 105 | case '-s': 106 | case '--stack-name': 107 | stack = getArgOrExit(++i, args); 108 | break; 109 | default: 110 | console.log(`Invalid argument ${args[i]}`); 111 | usage(); 112 | process.exit(1); 113 | } 114 | ++i; 115 | } 116 | if (!stack || !appName || !bucket || !region) { 117 | console.log('Missing required parameters'); 118 | usage(); 119 | process.exit(1); 120 | } 121 | } 122 | 123 | function spawnOrFail(command, args, options) { 124 | options = { 125 | ...options, 126 | shell: true 127 | }; 128 | console.log(`--> ${command} ${args.join(' ')}`); 129 | const cmd = spawnSync(command, args, options); 130 | if (cmd.error) { 131 | console.log(`Command ${command} failed with ${cmd.error.code}`); 132 | process.exit(255); 133 | } 134 | const output = cmd.stdout.toString(); 135 | console.log(output); 136 | if (cmd.status !== 0) { 137 | console.log( 138 | `Command ${command} failed with exit code ${cmd.status} signal ${cmd.signal}` 139 | ); 140 | console.log(cmd.stderr.toString()); 141 | process.exit(cmd.status); 142 | } 143 | return output; 144 | } 145 | 146 | function spawnAndIgnoreResult(command, args, options) { 147 | console.log(`--> ${command} ${args.join(' ')}`); 148 | spawnSync(command, args, options); 149 | } 150 | 151 | function appHtml(appName) { 152 | return `../browser/dist/${appName}.html`; 153 | } 154 | 155 | function setupCloud9() {} 156 | 157 | function ensureTools() { 158 | spawnOrFail('aws', ['--version']); 159 | spawnOrFail('sam', ['--version']); 160 | spawnOrFail('npm', ['install', '-g', 'yarn']); 161 | } 162 | 163 | function main() { 164 | parseArgs(); 165 | ensureTools(); 166 | 167 | const rootDir = `${__dirname}/..`; 168 | 169 | process.chdir(rootDir); 170 | 171 | spawnOrFail('script/cloud9-resize.sh', []); 172 | 173 | process.chdir(`${rootDir}/serverless`); 174 | 175 | if (!fs.existsSync('build')) { 176 | fs.mkdirSync('build'); 177 | } 178 | 179 | console.log(`Using region ${region}, bucket ${bucket}, stack ${stack}`); 180 | ensureBucket(bucket, false); 181 | ensureBucket(`${bucket}-releases`, true); 182 | 183 | const cssStyle = fs.readFileSync(`${rootDir}/resources/download.css`, 'utf8'); 184 | fs.writeFileSync( 185 | 'src/index.html', 186 | ` 187 | 188 | 189 | 190 | 191 | Download ${appName} 192 | 195 | 196 | 197 | 204 | 205 | 206 | ` 207 | ); 208 | 209 | const packageJson = JSON.parse( 210 | fs.readFileSync(`${rootDir}/package.json`, 'utf8') 211 | ); 212 | packageJson.productName = appName; 213 | packageJson.build.productName = appName; 214 | packageJson.build.appId = `com.amazonaws.services.chime.sdk.classroom.demo.${appName}`; 215 | fs.writeFileSync( 216 | `${rootDir}/package.json`, 217 | JSON.stringify(packageJson, null, 2) 218 | ); 219 | 220 | let mainDevTs = fs.readFileSync(`${rootDir}/app/main.dev.ts`, 'utf8'); 221 | mainDevTs = mainDevTs.replace(/setTitle.*?[;]/g, `setTitle('${appName}');`); 222 | fs.writeFileSync(`${rootDir}/app/main.dev.ts`, mainDevTs); 223 | 224 | let appHtml = fs.readFileSync(`${rootDir}/app/app.html`, 'utf8'); 225 | appHtml = appHtml.replace( 226 | /[<]title[>].*?[<][/]title[>]/g, 227 | `${appName}` 228 | ); 229 | fs.writeFileSync(`${rootDir}/app/app.html`, appHtml); 230 | 231 | spawnOrFail('sam', [ 232 | 'package', 233 | '--s3-bucket', 234 | `${bucket}`, 235 | `--output-template-file`, 236 | `build/packaged.yaml`, 237 | '--region', 238 | `${region}` 239 | ]); 240 | console.log('Deploying serverless application'); 241 | spawnOrFail('sam', [ 242 | 'deploy', 243 | '--template-file', 244 | './build/packaged.yaml', 245 | '--stack-name', 246 | `${stack}`, 247 | '--capabilities', 248 | 'CAPABILITY_IAM', 249 | '--region', 250 | `${region}` 251 | ]); 252 | const endpoint = spawnOrFail('aws', [ 253 | 'cloudformation', 254 | 'describe-stacks', 255 | '--stack-name', 256 | `${stack}`, 257 | '--query', 258 | 'Stacks[0].Outputs[0].OutputValue', 259 | '--output', 260 | 'text', 261 | '--region', 262 | `${region}` 263 | ]).trim(); 264 | const messagingWssUrl = spawnOrFail('aws', [ 265 | 'cloudformation', 266 | 'describe-stacks', 267 | '--stack-name', 268 | `${stack}`, 269 | '--query', 270 | 'Stacks[0].Outputs[1].OutputValue', 271 | '--output', 272 | 'text', 273 | '--region', 274 | `${region}` 275 | ]).trim(); 276 | console.log(`Endpoint: ${endpoint}`); 277 | console.log(`Messaging WSS URL: ${messagingWssUrl}`); 278 | 279 | process.chdir(rootDir); 280 | 281 | fs.writeFileSync( 282 | 'app/utils/getBaseUrl.ts', 283 | ` 284 | export default function getBaseUrl() {return '${endpoint}';} 285 | ` 286 | ); 287 | 288 | fs.writeFileSync( 289 | 'app/utils/getMessagingWssUrl.ts', 290 | ` 291 | export default function getMessagingWssUrl() {return '${messagingWssUrl}';} 292 | ` 293 | ); 294 | 295 | spawnOrFail('yarn', []); 296 | 297 | console.log('... packaging (this may take a while) ...'); 298 | spawnAndIgnoreResult('yarn', ['package-mac']); 299 | spawnAndIgnoreResult('yarn', ['package-win']); 300 | spawnOrFail('rm', ['-rf', `release/${appName}`]); 301 | spawnOrFail('mv', ['release/win-unpacked', `release/${appName}`]); 302 | process.chdir(`${rootDir}/release`); 303 | spawnOrFail('zip', ['-r', `${appName}-win.zip`, appName]); 304 | process.chdir(rootDir); 305 | 306 | console.log('... uploading Mac installer (this may take a while) ...'); 307 | spawnOrFail('aws', [ 308 | 's3', 309 | 'cp', 310 | '--acl', 311 | 'public-read', 312 | `release/${appName}.zip`, 313 | `s3://${bucket}-releases/mac/${appName}.zip` 314 | ]); 315 | 316 | console.log('... uploading Windows installer (this may take a while) ...'); 317 | spawnOrFail('aws', [ 318 | 's3', 319 | 'cp', 320 | '--acl', 321 | 'public-read', 322 | `release/${appName}-win.zip`, 323 | `s3://${bucket}-releases/win/${appName}.zip` 324 | ]); 325 | 326 | console.log('============================================================='); 327 | console.log(''); 328 | console.log('Link to download application:'); 329 | console.log(endpoint); 330 | console.log(''); 331 | console.log('============================================================='); 332 | } 333 | 334 | main(); 335 | -------------------------------------------------------------------------------- /serverless/.gitignore: -------------------------------------------------------------------------------- 1 | src/aws-sdk 2 | src/index.html 3 | src/indexV2.html 4 | -------------------------------------------------------------------------------- /serverless/src/handlers.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var ddb = new AWS.DynamoDB(); 3 | const chime = new AWS.Chime({ region: 'us-east-1' }); 4 | chime.endpoint = new AWS.Endpoint('https://service.chime.aws.amazon.com/console'); 5 | 6 | const oneDayFromNow = Math.floor(Date.now() / 1000) + 60 * 60 * 24; 7 | 8 | // Read resource names from the environment 9 | const meetingsTableName = process.env.MEETINGS_TABLE_NAME; 10 | const attendeesTableName = process.env.ATTENDEES_TABLE_NAME; 11 | 12 | function uuid() { 13 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 14 | var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); 15 | return v.toString(16); 16 | }); 17 | } 18 | 19 | const getMeeting = async(meetingTitle) => { 20 | const result = await ddb.getItem({ 21 | TableName: meetingsTableName, 22 | Key: { 23 | 'Title': { 24 | S: meetingTitle 25 | }, 26 | }, 27 | }).promise(); 28 | if (!result.Item) { 29 | return null; 30 | } 31 | const meetingData = JSON.parse(result.Item.Data.S); 32 | try { 33 | await chime.getMeeting({ 34 | MeetingId: meetingData.Meeting.MeetingId 35 | }).promise(); 36 | } catch (err) { 37 | return null; 38 | } 39 | return meetingData; 40 | } 41 | 42 | const putMeeting = async(title, meetingInfo) => { 43 | await ddb.putItem({ 44 | TableName: meetingsTableName, 45 | Item: { 46 | 'Title': { S: title }, 47 | 'Data': { S: JSON.stringify(meetingInfo) }, 48 | 'TTL': { 49 | N: '' + oneDayFromNow 50 | } 51 | } 52 | }).promise(); 53 | } 54 | 55 | const getAttendee = async(title, attendeeId) => { 56 | const result = await ddb.getItem({ 57 | TableName: attendeesTableName, 58 | Key: { 59 | 'AttendeeId': { 60 | S: `${title}/${attendeeId}` 61 | } 62 | } 63 | }).promise(); 64 | if (!result.Item) { 65 | return 'Unknown'; 66 | } 67 | return result.Item.Name.S; 68 | } 69 | 70 | const putAttendee = async(title, attendeeId, name) => { 71 | await ddb.putItem({ 72 | TableName: attendeesTableName, 73 | Item: { 74 | 'AttendeeId': { 75 | S: `${title}/${attendeeId}` 76 | }, 77 | 'Name': { S: name }, 78 | 'TTL': { 79 | N: '' + oneDayFromNow 80 | } 81 | } 82 | }).promise(); 83 | } 84 | 85 | function simplifyTitle(title) { 86 | // Strip out most symbolic characters and whitespace and make case insensitive, 87 | // but preserve any Unicode characters outside of the ASCII range. 88 | return (title || '').replace(/[\s()!@#$%^&*`~_=+{}|\\;:'",.<>/?\[\]-]+/gu, '').toLowerCase() || null; 89 | } 90 | 91 | // ===== Join or create meeting =================================== 92 | exports.createMeeting = async(event, context, callback) => { 93 | var response = { 94 | "statusCode": 200, 95 | "headers": {}, 96 | "body": '', 97 | "isBase64Encoded": false 98 | }; 99 | event.queryStringParameters.title = simplifyTitle(event.queryStringParameters.title); 100 | if (!event.queryStringParameters.title) { 101 | response["statusCode"] = 400; 102 | response["body"] = "Must provide title"; 103 | callback(null, response); 104 | return; 105 | } 106 | const title = event.queryStringParameters.title; 107 | const region = event.queryStringParameters.region || 'us-east-1'; 108 | let meetingInfo = await getMeeting(title); 109 | if (!meetingInfo) { 110 | const request = { 111 | ClientRequestToken: uuid(), 112 | MediaRegion: region, 113 | }; 114 | console.info('Creating new meeting: ' + JSON.stringify(request)); 115 | meetingInfo = await chime.createMeeting(request).promise(); 116 | await putMeeting(title, meetingInfo); 117 | } 118 | 119 | const joinInfo = { 120 | JoinInfo: { 121 | Title: title, 122 | Meeting: meetingInfo.Meeting, 123 | }, 124 | }; 125 | 126 | response.body = JSON.stringify(joinInfo, '', 2); 127 | callback(null, response); 128 | }; 129 | 130 | exports.join = async(event, context, callback) => { 131 | var response = { 132 | "statusCode": 200, 133 | "headers": {}, 134 | "body": '', 135 | "isBase64Encoded": false 136 | }; 137 | 138 | event.queryStringParameters.title = simplifyTitle(event.queryStringParameters.title); 139 | if (!event.queryStringParameters.title || !event.queryStringParameters.name) { 140 | response["statusCode"] = 400; 141 | response["body"] = "Must provide title and name"; 142 | callback(null, response); 143 | return; 144 | } 145 | const title = event.queryStringParameters.title; 146 | const name = event.queryStringParameters.name; 147 | const region = event.queryStringParameters.region || 'us-east-1'; 148 | let meetingInfo = await getMeeting(title); 149 | if (!meetingInfo && event.queryStringParameters.role !== 'student') { 150 | const request = { 151 | ClientRequestToken: uuid(), 152 | MediaRegion: region, 153 | }; 154 | console.info('Creating new meeting: ' + JSON.stringify(request)); 155 | meetingInfo = await chime.createMeeting(request).promise(); 156 | await putMeeting(title, meetingInfo); 157 | } 158 | 159 | console.info('Adding new attendee'); 160 | const attendeeInfo = (await chime.createAttendee({ 161 | MeetingId: meetingInfo.Meeting.MeetingId, 162 | ExternalUserId: uuid(), 163 | }).promise()); 164 | 165 | putAttendee(title, attendeeInfo.Attendee.AttendeeId, name); 166 | 167 | const joinInfo = { 168 | JoinInfo: { 169 | Title: title, 170 | Meeting: meetingInfo.Meeting, 171 | Attendee: attendeeInfo.Attendee 172 | }, 173 | }; 174 | 175 | response.body = JSON.stringify(joinInfo, '', 2); 176 | callback(null, response); 177 | }; 178 | 179 | exports.attendee = async(event, context, callback) => { 180 | var response = { 181 | "statusCode": 200, 182 | "headers": {}, 183 | "body": '', 184 | "isBase64Encoded": false 185 | }; 186 | event.queryStringParameters.title = simplifyTitle(event.queryStringParameters.title); 187 | const title = event.queryStringParameters.title; 188 | const attendeeId = event.queryStringParameters.attendee; 189 | const attendeeInfo = { 190 | AttendeeInfo: { 191 | AttendeeId: attendeeId, 192 | Name: await getAttendee(title, attendeeId), 193 | }, 194 | }; 195 | response.body = JSON.stringify(attendeeInfo, '', 2); 196 | callback(null, response); 197 | }; 198 | 199 | exports.end = async(event, context, callback) => { 200 | var response = { 201 | "statusCode": 200, 202 | "headers": {}, 203 | "body": '', 204 | "isBase64Encoded": false 205 | }; 206 | event.queryStringParameters.title = simplifyTitle(event.queryStringParameters.title); 207 | const title = event.queryStringParameters.title; 208 | let meetingInfo = await getMeeting(title); 209 | await chime.deleteMeeting({ 210 | MeetingId: meetingInfo.Meeting.MeetingId, 211 | }).promise(); 212 | callback(null, response); 213 | }; 214 | -------------------------------------------------------------------------------- /serverless/src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | exports.handler = async (event, context, callback) => { 4 | const response = { 5 | statusCode: 200, 6 | headers: { 7 | 'Content-Type': 'text/html' 8 | }, 9 | body: '', 10 | isBase64Encoded: false 11 | }; 12 | response.body = fs.readFileSync('./index.html', { encoding: 'utf8' }); 13 | callback(null, response); 14 | }; 15 | -------------------------------------------------------------------------------- /serverless/src/messaging.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const AWS = require('aws-sdk'); 5 | 6 | const ddb = new AWS.DynamoDB({ region: process.env.AWS_REGION }); 7 | const chime = new AWS.Chime({ region: 'us-east-1' }); 8 | chime.endpoint = new AWS.Endpoint( 9 | 'https://service.chime.aws.amazon.com/console' 10 | ); 11 | const { CONNECTIONS_TABLE_NAME } = process.env; 12 | const strictVerify = true; 13 | 14 | exports.authorize = async (event, context, callback) => { 15 | console.log('authorize event:', JSON.stringify(event, null, 2)); 16 | const generatePolicy = (principalId, effect, resource, context) => { 17 | const authResponse = {}; 18 | authResponse.principalId = principalId; 19 | if (effect && resource) { 20 | const policyDocument = {}; 21 | policyDocument.Version = '2012-10-17'; 22 | policyDocument.Statement = []; 23 | const statementOne = {}; 24 | statementOne.Action = 'execute-api:Invoke'; 25 | statementOne.Effect = effect; 26 | statementOne.Resource = resource; 27 | policyDocument.Statement[0] = statementOne; 28 | authResponse.policyDocument = policyDocument; 29 | } 30 | authResponse.context = context; 31 | return authResponse; 32 | }; 33 | let passedAuthCheck = false; 34 | if ( 35 | !!event.queryStringParameters.MeetingId && 36 | !!event.queryStringParameters.AttendeeId && 37 | !!event.queryStringParameters.JoinToken 38 | ) { 39 | try { 40 | attendeeInfo = await chime 41 | .getAttendee({ 42 | MeetingId: event.queryStringParameters.MeetingId, 43 | AttendeeId: event.queryStringParameters.AttendeeId 44 | }) 45 | .promise(); 46 | if ( 47 | attendeeInfo.Attendee.JoinToken === 48 | event.queryStringParameters.JoinToken 49 | ) { 50 | passedAuthCheck = true; 51 | } else if (strictVerify) { 52 | console.error('failed to authenticate with join token'); 53 | } else { 54 | passedAuthCheck = true; 55 | console.warn( 56 | 'failed to authenticate with join token (skipping due to strictVerify=false)' 57 | ); 58 | } 59 | } catch (e) { 60 | if (strictVerify) { 61 | console.error(`failed to authenticate with join token: ${e.message}`); 62 | } else { 63 | passedAuthCheck = true; 64 | console.warn( 65 | `failed to authenticate with join token (skipping due to strictVerify=false): ${e.message}` 66 | ); 67 | } 68 | } 69 | } else { 70 | console.error('missing MeetingId, AttendeeId, JoinToken parameters'); 71 | } 72 | return generatePolicy( 73 | 'me', 74 | passedAuthCheck ? 'Allow' : 'Deny', 75 | event.methodArn, 76 | { 77 | MeetingId: event.queryStringParameters.MeetingId, 78 | AttendeeId: event.queryStringParameters.AttendeeId 79 | } 80 | ); 81 | }; 82 | 83 | exports.onconnect = async event => { 84 | console.log('onconnect event:', JSON.stringify(event, null, 2)); 85 | const oneDayFromNow = Math.floor(Date.now() / 1000) + 60 * 60 * 24; 86 | try { 87 | await ddb 88 | .putItem({ 89 | TableName: process.env.CONNECTIONS_TABLE_NAME, 90 | Item: { 91 | MeetingId: { S: event.requestContext.authorizer.MeetingId }, 92 | AttendeeId: { S: event.requestContext.authorizer.AttendeeId }, 93 | ConnectionId: { S: event.requestContext.connectionId }, 94 | TTL: { N: `${oneDayFromNow}` } 95 | } 96 | }) 97 | .promise(); 98 | } catch (e) { 99 | console.error(`error connecting: ${e.message}`); 100 | return { 101 | statusCode: 500, 102 | body: `Failed to connect: ${JSON.stringify(err)}` 103 | }; 104 | } 105 | return { statusCode: 200, body: 'Connected.' }; 106 | }; 107 | 108 | exports.ondisconnect = async event => { 109 | console.log('ondisconnect event:', JSON.stringify(event, null, 2)); 110 | try { 111 | await ddb 112 | .deleteItem({ 113 | TableName: process.env.CONNECTIONS_TABLE_NAME, 114 | Key: { 115 | MeetingId: { S: event.requestContext.authorizer.MeetingId }, 116 | AttendeeId: { S: event.requestContext.authorizer.AttendeeId }, 117 | }, 118 | }) 119 | .promise(); 120 | } catch (err) { 121 | return { 122 | statusCode: 500, 123 | body: `Failed to disconnect: ${JSON.stringify(err)}` 124 | }; 125 | } 126 | return { statusCode: 200, body: 'Disconnected.' }; 127 | }; 128 | 129 | exports.sendmessage = async event => { 130 | console.log('sendmessage event:', JSON.stringify(event, null, 2)); 131 | let attendees = {}; 132 | try { 133 | attendees = await ddb 134 | .query({ 135 | ExpressionAttributeValues: { 136 | ':meetingId': { S: event.requestContext.authorizer.MeetingId } 137 | }, 138 | KeyConditionExpression: 'MeetingId = :meetingId', 139 | ProjectionExpression: 'ConnectionId', 140 | TableName: CONNECTIONS_TABLE_NAME 141 | }) 142 | .promise(); 143 | } catch (e) { 144 | return { statusCode: 500, body: e.stack }; 145 | } 146 | const apigwManagementApi = new AWS.ApiGatewayManagementApi({ 147 | apiVersion: '2018-11-29', 148 | endpoint: `${event.requestContext.domainName}/${event.requestContext.stage}` 149 | }); 150 | const postData = JSON.parse(event.body).data; 151 | const postCalls = attendees.Items.map(async connection => { 152 | const connectionId = connection.ConnectionId.S; 153 | try { 154 | await apigwManagementApi 155 | .postToConnection({ ConnectionId: connectionId, Data: postData }) 156 | .promise(); 157 | } catch (e) { 158 | if (e.statusCode === 410) { 159 | console.log(`found stale connection, skipping ${connectionId}`); 160 | } else { 161 | console.error( 162 | `error posting to connection ${connectionId}: ${e.message}` 163 | ); 164 | } 165 | } 166 | }); 167 | try { 168 | await Promise.all(postCalls); 169 | } catch (e) { 170 | console.error(`failed to post: ${e.message}`); 171 | return { statusCode: 500, body: e.stack }; 172 | } 173 | return { statusCode: 200, body: 'Data sent.' }; 174 | }; 175 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "CommonJS", 5 | "lib": ["dom", "esnext"], 6 | "declaration": true, 7 | "declarationMap": true, 8 | "noEmit": true, 9 | "jsx": "react", 10 | "strict": true, 11 | "pretty": true, 12 | "sourceMap": true, 13 | /* Additional Checks */ 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | /* Module Resolution Options */ 19 | "moduleResolution": "node", 20 | "esModuleInterop": true, 21 | "allowSyntheticDefaultImports": true, 22 | "resolveJsonModule": true, 23 | "allowJs": true, 24 | "skipLibCheck": true 25 | }, 26 | "exclude": [ 27 | "test", 28 | "release", 29 | "app/main.prod.js", 30 | "app/main.prod.js.map", 31 | "app/renderer.prod.js", 32 | "app/renderer.prod.js.map", 33 | "app/style.css", 34 | "app/style.css.map", 35 | "app/dist", 36 | "dll", 37 | "app/main.js", 38 | "app/main.js.map" 39 | ] 40 | } 41 | --------------------------------------------------------------------------------