([]);
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 |
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 |
80 |
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 |
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 |
46 |
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 |
48 |
54 | {raisedHand && (
55 |
56 |
62 | ✋
63 |
64 |
65 | )}
66 |
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 |
})
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 |
87 | {children}
88 |
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 |
198 | Download ${appName}
199 |
203 |
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 |
--------------------------------------------------------------------------------