Skip to content

Commit 3f3aabf

Browse files
committed
add controls for video conferences
1 parent b4cb7e4 commit 3f3aabf

File tree

10 files changed

+170
-60
lines changed

10 files changed

+170
-60
lines changed

services/app/apps/codebattle/assets/js/widgets/App.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ const {
2727
const { gameUI: gameUIReducer, ...otherReducers } = reducers;
2828

2929
const gameUIPersistWhitelist = [
30-
'audioMute',
31-
'videoMute',
30+
'audioMuted',
31+
'videoMuted',
3232
'showVideoConferencePanel',
3333
'editorMode',
3434
'editorTheme',

services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,8 @@ Editor.propTypes = {
5656
editable: PropTypes.bool,
5757
roomMode: PropTypes.string.isRequired,
5858
checkResult: PropTypes.func.isRequired,
59-
toggleMuteSound: PropTypes.func.isRequired,
60-
mute: PropTypes.bool.isRequired,
6159
userType: PropTypes.string.isRequired,
62-
userId: PropTypes.string.isRequired,
63-
onChangeCursorSelection: PropTypes.func.isRequired,
64-
onChangeCursorPosition: PropTypes.func.isRequired,
60+
userId: PropTypes.number.isRequired,
6561
};
6662

6763
Editor.defaultProps = {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const statuses = {
2+
loading: 'loading',
3+
ready: 'ready',
4+
joinedGameRoom: 'joinedGameRoom',
5+
notSupported: 'notSupported',
6+
noHaveApiKey: 'noHaveApiKey',
7+
};
8+
9+
export default statuses;

services/app/apps/codebattle/assets/js/widgets/pages/game/VideoConference.jsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import Loading from '@/components/Loading';
88
import useJitsiRoom from '@/utils/useJitsiRoom';
99

1010
import i18n from '../../../i18n';
11+
import statuses from '../../config/jitsiStatuses';
1112

1213
const mapStatusToDescription = {
13-
loading: i18n.t('Setup Conference Room'),
14-
ready: i18n.t('Conference Room Is Ready'),
15-
joinedGameRoom: i18n.t('Conference Room Is Started'),
16-
notSupported: i18n.t('Not Supported Browser'),
17-
noHaveApiKey: i18n.t('No have jitsi api key'),
14+
[statuses.loading]: i18n.t('Setup Conference Room'),
15+
[statuses.ready]: i18n.t('Conference Room Is Ready'),
16+
[statuses.joinedGameRoom]: i18n.t('Conference Room Is Started'),
17+
[statuses.notSupported]: i18n.t('Not Supported Browser'),
18+
[statuses.noHaveApiKey]: i18n.t('No have jitsi api key'),
1819
};
1920

2021
function ConferenceLoading({ status, hideLoader = false }) {
@@ -32,21 +33,23 @@ function VideoConference() {
3233
status,
3334
} = useJitsiRoom();
3435

36+
const loadingClassName = cn('w-100 h-100', {
37+
'd-flex justify-content-center align-items-center': status !== statuses.joinedGameRoom,
38+
'd-none invisible absolute': status === statuses.joinedGameRoom,
39+
});
3540
const conferenceClassName = cn('w-100 h-100', {
36-
'd-none invisible absolute': status !== 'joinedGameRoom',
41+
'd-none invisible absolute': status !== statuses.joinedGameRoom,
3742
});
3843

3944
return (
4045
<>
41-
{status !== 'joinedGameRoom' && (
42-
<div className="d-flex w-100 h-100 justify-content-center align-items-center">
43-
<ConferenceLoading
44-
status={status}
45-
hideLoader={['notSupported', 'noHaveApiKey'].includes(status)}
46-
/>
47-
</div>
48-
)}
49-
<div ref={ref} id="jaas-container" className={conferenceClassName} />
46+
<div className={loadingClassName}>
47+
<ConferenceLoading
48+
status={status}
49+
hideLoader={[statuses.notSupported, statuses.noHaveApiKey].includes(status)}
50+
/>
51+
</div>
52+
<div ref={ref} className={conferenceClassName} />
5053
</>
5154
);
5255
}

services/app/apps/codebattle/assets/js/widgets/pages/game/VideoConferenceButton.jsx

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { actions } from '@/slices';
88
import i18n from '../../../i18n';
99
import * as selectors from '../../selectors';
1010

11+
import VideoConferenceMediaControls from './VideoConferenceMediaControls';
12+
1113
function VideoConferenceButton() {
1214
const dispatch = useDispatch();
13-
14-
// const { audioMute, videoMute } = useSelector(selectors.videoConferenceSettingsSelector);
1515
const showVideoConferencePanel = useSelector(selectors.showVideoConferencePanelSelector);
1616

1717
const toggleVideoConference = () => {
@@ -40,20 +40,9 @@ function VideoConferenceButton() {
4040
: i18n.t('Open Video Chat')
4141
}
4242
</button>
43-
{/* {showVideoConferencePanel && ( */}
44-
{/* <div className="d-flex"> */}
45-
{/* <button */}
46-
{/* type="button" */}
47-
{/* className="btn btn-secondary btn-block w-100 rounded-lg" */}
48-
{/* aria-label="Mute audio" */}
49-
{/* /> */}
50-
{/* <button */}
51-
{/* type="button" */}
52-
{/* className="btn btn-secondary btn-block w-100 rounded-lg" */}
53-
{/* aria-label="Mute video" */}
54-
{/* /> */}
55-
{/* </div> */}
56-
{/* )} */}
43+
{showVideoConferencePanel && (
44+
<VideoConferenceMediaControls />
45+
)}
5746
</>
5847
);
5948
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { useEffect } from 'react';
2+
3+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4+
import cn from 'classnames';
5+
import { useDispatch, useSelector } from 'react-redux';
6+
7+
import { actions } from '@/slices';
8+
9+
import * as selectors from '../../selectors';
10+
11+
function VideoConferenceMediaControls() {
12+
const dispatch = useDispatch();
13+
14+
const { audioMuted, videoMuted } = useSelector(selectors.videoConferenceSettingsSelector);
15+
const { audioAvailable, videoAvailable } = useSelector(selectors.videoConferenceMediaAvailableSelector);
16+
17+
useEffect(() => () => {
18+
dispatch(actions.setVideoAvailable(false));
19+
dispatch(actions.setAudioAvailable(false));
20+
}, [dispatch]);
21+
22+
const audioMuteBtnClassName = cn('btn btn-secondary w-100 h-100 rounded-left', {
23+
disabled: !audioAvailable,
24+
});
25+
const videoMuteBtnClassName = cn('btn btn-secondary w-100 h-100 rounded-right', {
26+
disabled: !videoAvailable,
27+
});
28+
29+
return (
30+
<div className="d-flex btn-block mt-2">
31+
<button
32+
type="button"
33+
className={audioMuteBtnClassName}
34+
aria-label="Mute audio"
35+
onClick={() => dispatch(actions.setAudioMuted(!audioMuted))}
36+
disabled={!audioAvailable}
37+
>
38+
<FontAwesomeIcon icon={audioMuted ? 'microphone-slash' : 'microphone'} />
39+
</button>
40+
<button
41+
type="button"
42+
className={videoMuteBtnClassName}
43+
aria-label="Mute video"
44+
onClick={() => dispatch(actions.setVideoMuted(!videoMuted))}
45+
disabled={!videoAvailable}
46+
>
47+
<FontAwesomeIcon icon={videoMuted ? 'video-slash' : 'video'} />
48+
</button>
49+
</div>
50+
);
51+
}
52+
53+
export default VideoConferenceMediaControls;

services/app/apps/codebattle/assets/js/widgets/selectors/index.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export const currentUserIsAdminSelector = state => !!state.user.users[state.user
2525

2626
export const currentUserIsGuestSelector = state => !!state.user.users[state.user.currentUserId].isGuest;
2727

28+
export const userByIdSelector = userId => state => state.user.users[userId];
29+
2830
export const userIsAdminSelector = userId => state => !!state.user.users[userId]?.isAdmin;
2931

3032
export const subscriptionTypeSelector = state => (
@@ -450,12 +452,21 @@ export const currentChatUserSelector = state => {
450452

451453
export const taskDescriptionLanguageSelector = state => state.gameUI.taskDescriptionLanguage;
452454

455+
export const videoConferenceMediaAvailableSelector = createDraftSafeSelector(
456+
state => state.gameUI.audioAvailable,
457+
state => state.gameUI.videoAvailable,
458+
(audioAvailable, videoAvailable) => ({
459+
audioAvailable,
460+
videoAvailable,
461+
}),
462+
);
463+
453464
export const videoConferenceSettingsSelector = createDraftSafeSelector(
454-
state => state.gameUI.audioMute,
455-
state => state.gameUI.videoMute,
456-
(audioMute, videoMute) => ({
457-
audioMute,
458-
videoMute,
465+
state => state.gameUI.audioMuted,
466+
state => state.gameUI.videoMuted,
467+
(audioMuted, videoMuted) => ({
468+
audioMuted,
469+
videoMuted,
459470
}),
460471
);
461472

services/app/apps/codebattle/assets/js/widgets/slices/gameUI.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ const initialState = {
1515
showToastActionsAfterGame: false,
1616
isShowGuide: false,
1717
showVideoConferencePanel: false,
18-
videoMute: true,
19-
audioMute: true,
18+
videoMuted: false,
19+
audioMuted: false,
20+
audioAvailable: false,
21+
videoAvailable: false,
2022
};
2123

2224
const gameUI = createSlice({
@@ -52,11 +54,17 @@ const gameUI = createSlice({
5254
toggleShowVideoConferencePanel: state => {
5355
state.showVideoConferencePanel = !state.showVideoConferencePanel;
5456
},
55-
setAudioMute: (state, payload) => {
56-
state.audioMute = payload;
57+
setAudioMuted: (state, { payload }) => {
58+
state.audioMuted = payload;
5759
},
58-
setVideoMute: (state, payload) => {
59-
state.videoMute = payload;
60+
setVideoMuted: (state, { payload }) => {
61+
state.videoMuted = payload;
62+
},
63+
setAudioAvailable: (state, { payload }) => {
64+
state.audioAvailable = payload;
65+
},
66+
setVideoAvailable: (state, { payload }) => {
67+
state.videoAvailable = payload;
6068
},
6169
},
6270
});

services/app/apps/codebattle/assets/js/widgets/utils/useEditor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ const useOption = (editor, {
109109
* toggleMuteSound: Function,
110110
* mute: boolean,
111111
* userType: string,
112-
* userId: string,
112+
* userId: number,
113113
* onChangeCursorSelection: Function,
114114
* onChangeCursorPosition: Function,
115115
* }} props

services/app/apps/codebattle/assets/js/widgets/utils/useJitsiRoom.js

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useDispatch, useSelector } from 'react-redux';
88

99
import { actions } from '@/slices';
1010

11+
import statuses from '../config/jitsiStatuses';
1112
import * as selectors from '../selectors';
1213

1314
const apiKey = Gon.getAsset('jitsi_api_key');
@@ -16,10 +17,16 @@ const useJitsiRoom = () => {
1617
const dispatch = useDispatch();
1718

1819
const ref = useRef();
19-
const [status, setStatus] = useState('loading');
20+
const [api, setApi] = useState(null);
21+
const [status, setStatus] = useState(statuses.loading);
2022
const userId = useSelector(selectors.currentUserIdSelector);
2123
const gameId = useSelector(selectors.gameIdSelector);
22-
const { name } = useSelector(state => state.user.users[userId]);
24+
const { name } = useSelector(selectors.userByIdSelector(userId));
25+
26+
const {
27+
audioMuted,
28+
videoMuted,
29+
} = useSelector(selectors.videoConferenceSettingsSelector);
2330

2431
const roomName = gameId ? `${apiKey}/codebattle_game_${gameId}` : `${apiKey}/codebattle_testing`;
2532

@@ -29,49 +36,83 @@ const useJitsiRoom = () => {
2936
}
3037

3138
if (!apiKey) {
32-
setStatus('noHaveApiKey');
39+
setStatus(statuses.noHaveApiKey);
3340
}
3441
}, [dispatch]);
3542

3643
useEffect(() => {
37-
if (status === 'loading' && JitsiMeetExternalAPI && apiKey) {
44+
if (status === statuses.loading && JitsiMeetExternalAPI && apiKey) {
3845
const newApi = new JitsiMeetExternalAPI('8x8.vc', {
3946
roomName,
4047
parentNode: ref.current,
4148
userInfo: {
4249
displayName: name,
4350
},
4451
configOverwrite: {
52+
startWithAudioMuted: audioMuted,
53+
startWithVideoMuted: videoMuted,
4554
prejoinPageEnabled: false,
4655
hideConferenceSubject: true,
4756
// hideConferenceTimer: true,
4857
toolbarButtons: [
49-
'camera',
50-
'microphone',
5158
'settings',
5259
],
5360
},
5461
});
5562

5663
newApi.addListener('browserSupport', payload => {
5764
if (payload.supported) {
58-
setStatus('ready');
65+
setStatus(statuses.ready);
5966
} else {
60-
setStatus('notSupported');
67+
setStatus(statuses.notSupported);
6168
}
6269
});
6370

6471
newApi.addListener('videoConferenceJoined', () => {
65-
setStatus('joinedGameRoom');
72+
newApi.getAvailableDevices().then(devices => {
73+
const { audioInput, videoInput } = devices;
74+
75+
const audioAvailable = audioInput.some(item => !!item.deviceId);
76+
const videoAvailable = videoInput.some(item => !!item.deviceId);
77+
78+
dispatch(actions.setAudioAvailable(audioAvailable));
79+
dispatch(actions.setVideoAvailable(videoAvailable));
80+
});
81+
82+
setStatus(statuses.joinedGameRoom);
6683
});
84+
85+
setApi(newApi);
6786
}
6887
// eslint-disable-next-line react-hooks/exhaustive-deps
6988
}, [status]);
7089

90+
useEffect(() => {
91+
if (api) {
92+
api.isAudioMuted().then(muted => {
93+
if (muted !== audioMuted) {
94+
api.executeCommand('toggleAudio');
95+
}
96+
});
97+
}
98+
// eslint-disable-next-line react-hooks/exhaustive-deps
99+
}, [audioMuted]);
100+
101+
useEffect(() => {
102+
if (api) {
103+
api.isVideoMuted().then(muted => {
104+
if (muted !== videoMuted) {
105+
api.executeCommand('toggleVideo');
106+
}
107+
});
108+
}
109+
// eslint-disable-next-line react-hooks/exhaustive-deps
110+
}, [videoMuted]);
111+
71112
return useMemo(() => ({
72113
ref,
73114
status,
74-
}), [ref, status]);
115+
}), [status]);
75116
};
76117

77118
export default useJitsiRoom;

0 commit comments

Comments
 (0)