diff --git a/app/package.json b/app/package.json index 5cd59a99..07740afa 100644 --- a/app/package.json +++ b/app/package.json @@ -43,7 +43,7 @@ "is-electron": "^2.2.0", "marked": "^0.8.0", "material-ui-popup-state": "^1.8.0", - "mediasoup-client": "^3.6.55", + "mediasoup-client": "^3.7.1", "notistack": "^0.9.5", "prop-types": "^15.7.2", "random-string": "^0.2.0", @@ -51,6 +51,7 @@ "react-cookie-consent": "^2.5.0", "react-dom": "^16.10.2", "react-flip-toolkit": "^7.0.9", + "react-iframe": "1.8.5", "react-image-file-resizer": "^0.3.8", "react-images-upload": "^1.2.0", "react-intl": "^3.4.0", @@ -66,7 +67,7 @@ "redux-thunk": "^2.3.0", "reselect": "^4.0.0", "riek": "^1.1.0", - "socket.io-client": "^2.4.0", + "socket.io-client": "^4.5.4", "source-map-explorer": "^2.1.0", "streamsaver": "^2.0.5", "typescript": "^4.2.4", diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index f685d574..7533a87f 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -2110,6 +2110,24 @@ export default class RoomClient peerActions.setStopPeerScreenSharingInProgress(peerId, false)); } + async toggleIframe(iframeUrl) + { + store.dispatch( + roomActions.setToggleIframeInProgress(true)); + + try + { + await this.sendRequest('toggleIframe', { iframeUrl }); + } + catch (error) + { + logger.error('toggleIframe() [error:"%o"]', error); + } + + store.dispatch( + roomActions.setToggleIframeInProgress(false)); + } + async muteAllPeers() { logger.debug('muteAllPeers()'); @@ -3194,6 +3212,24 @@ export default class RoomClient break; } + case 'showIframe': + { + const { iframeUrl } = notification.data; + + store.dispatch( + roomActions.openIframe(iframeUrl)); + + break; + } + + case 'closeIframe': + { + store.dispatch( + roomActions.closeIframe()); + + break; + } + case 'moderator:clearChat': { store.dispatch(chatActions.clearChat()); @@ -3917,6 +3953,7 @@ export default class RoomClient chatHistory, fileHistory, lastNHistory, + iframeHistory, locked, lobbyPeers, accessCode @@ -3984,6 +4021,9 @@ export default class RoomClient (fileHistory.length > 0) && store.dispatch( fileActions.addFileHistory(fileHistory)); + (iframeHistory) && store.dispatch( + roomActions.openIframe(iframeHistory)); + locked ? store.dispatch(roomActions.setRoomLocked()) : store.dispatch(roomActions.setRoomUnLocked()); diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index 7077d8db..aa6d9768 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -3,7 +3,8 @@ import { connect } from 'react-redux'; import { meProducersSelector, makePermissionSelector, - recordingConsentsPeersSelector + recordingConsentsPeersSelector, + showIframeSelect } from '../../store/selectors'; import { permissions } from '../../permissions'; import { withRoomContext } from '../../RoomContext'; @@ -60,6 +61,10 @@ const styles = (theme) => '&.screen' : { order : 2 + }, + '&.iframe' : + { + order : 3 } }, viewContainer : @@ -156,6 +161,7 @@ const Me = (props) => roomClient, me, settings, + iframeUrl, activeSpeaker, spacing, style, @@ -175,7 +181,7 @@ const Me = (props) => localRecordingState } = props; - // const width = style.width; + const width = style.width; const height = style.height; @@ -1018,6 +1024,19 @@ const Me = (props) => } + {iframeUrl && +
+
+ +
+
+ + } ); }; @@ -1028,6 +1047,7 @@ Me.propTypes = advancedMode : PropTypes.bool, me : appPropTypes.Me.isRequired, settings : PropTypes.object, + iframeUrl : PropTypes.string.isRequired, activeSpeaker : PropTypes.bool, micProducer : appPropTypes.Producer, webcamProducer : appPropTypes.Producer, @@ -1073,6 +1093,7 @@ const makeMapStateToProps = () => me : state.me, ...meProducersSelector(state), settings : state.settings, + iframeUrl : showIframeSelect(state), activeSpeaker : state.me.id === state.room.activeSpeakerId, hasAudioPermission : canShareAudio(state), hasVideoPermission : canShareVideo(state), diff --git a/app/src/components/JoinDialog.js b/app/src/components/JoinDialog.js index dd070d00..5a875c9b 100644 --- a/app/src/components/JoinDialog.js +++ b/app/src/components/JoinDialog.js @@ -303,24 +303,27 @@ const JoinDialog = ({ }; // TODO: prefix with the Edumeet server HTTP endpoint - fetch('/auth/check_login_status', { - credentials : 'include', - method : 'GET', - cache : 'no-cache', - redirect : 'follow', - referrerPolicy : 'no-referrer' }) - .then((response) => response.json()) - .then((json) => - { - if (json.loggedIn) + if (config.loginEnabled) + { + fetch('/auth/check_login_status', { + credentials : 'include', + method : 'GET', + cache : 'no-cache', + redirect : 'follow', + referrerPolicy : 'no-referrer' }) + .then((response) => response.json()) + .then((json) => { - roomClient.setLoggedIn(json.loggedIn); - } - }) - .catch((error) => - { - logger.error('Error checking login status', error); - }); + if (json.loggedIn) + { + roomClient.setLoggedIn(json.loggedIn); + } + }) + .catch((error) => + { + logger.error('Error checking login status', error); + }); + } return (
diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js index a3c7825a..06fd6000 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListMe.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListMe.js @@ -1,19 +1,41 @@ -import React from 'react'; +import React, { useState } from 'react'; import { connect } from 'react-redux'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import * as appPropTypes from '../../appPropTypes'; -import { useIntl } from 'react-intl'; +import { useIntl, FormattedMessage } from 'react-intl'; import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; import PanIcon from '@material-ui/icons/PanTool'; +import Button from '@material-ui/core/Button'; import EmptyAvatar from '../../../images/avatar-empty.jpeg'; +import TextField from '@material-ui/core/TextField'; +import { showIframeSelect, makePermissionSelector } from '../../../store/selectors'; +import { permissions } from '../../../permissions'; +import { config } from '../../../config'; + +const urlPattern = new RegExp( + '^(https?:\\/\\/)?' + + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + + '((\\d{1,3}\\.){3}\\d{1,3}))' + + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + + '(\\?[;&a-z\\d%_.~+=-]*)?' + + '(\\#[-a-z\\d_]*)?$', + 'i' +); const styles = (theme) => ({ root : + { + display : 'flex', + flexDirection : 'column', + width : '100%', + overflowY : 'auto' + }, + me : { width : '100%', overflow : 'hidden', @@ -48,70 +70,163 @@ const styles = (theme) => const ListMe = (props) => { - const intl = useIntl(); - const { roomClient, me, + iframeUrl, + toggleIframeInProgress, + hasScreenPermission, settings, classes } = props; + const intl = useIntl(); + + const [ currentUrl, setCurrentUrl ] = useState(''); + + const validateUrl = () => + { + if (currentUrl === '') + return false; + + if (!urlPattern.test(currentUrl)) + return false; + + if (!currentUrl.toLowerCase().startsWith('https://')) + return false; + + return true; + }; + + const isValidUrl = validateUrl(); + const picture = me.picture || EmptyAvatar; return (
- My avatar - -
- {settings.displayName} -
- - + My avatar +
+ {settings.displayName} +
+ - { - e.stopPropagation(); + placement='bottom' + > + + { + e.stopPropagation(); + roomClient.setRaisedHand(!me.raisedHand); + }} + > + + + +
- roomClient.setRaisedHand(!me.raisedHand); + {hasScreenPermission && + + setCurrentUrl(event.target.value.trim())} + fullWidth + /> + {iframeUrl && + + } + {!iframeUrl && + + } + + }
); }; ListMe.propTypes = { - roomClient : PropTypes.object.isRequired, - me : appPropTypes.Me.isRequired, - settings : PropTypes.object.isRequired, - classes : PropTypes.object.isRequired + roomClient : PropTypes.object.isRequired, + me : appPropTypes.Me.isRequired, + iframeUrl : PropTypes.string, + toggleIframeInProgress : PropTypes.bool, + hasScreenPermission : PropTypes.bool.isRequired, + settings : PropTypes.object.isRequired, + classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => ({ - me : state.me, - settings : state.settings -}); +const makeMapStateToProps = () => +{ + const canShareScreen = + makePermissionSelector(permissions.SHARE_SCREEN); + + const mapStateToProps = (state) => ({ + me : state.me, + iframeUrl : showIframeSelect(state), + toggleIframeInProgress : state.room.toggleIframeInProgress, + hasScreenPermission : canShareScreen(state), + settings : state.settings + }); + + return mapStateToProps; +}; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { @@ -119,6 +234,8 @@ export default withRoomContext(connect( { return ( prev.me === next.me && + prev.room.iframeUrl === next.room.iframeUrl && + prev.room.toggleIframeInProgress === next.room.toggleIframeInProgress && prev.settings === next.settings ); } diff --git a/app/src/components/MeetingViews/Democratic.js b/app/src/components/MeetingViews/Democratic.js index 7452be94..2497d950 100644 --- a/app/src/components/MeetingViews/Democratic.js +++ b/app/src/components/MeetingViews/Democratic.js @@ -243,6 +243,7 @@ export default connect( prev.room.spotlights === next.room.spotlights && prev.room.toolbarsVisible === next.room.toolbarsVisible && prev.room.hideSelfView === next.room.hideSelfView && + prev.room.iframeUrl === next.room.iframeUrl && prev.settings.permanentTopBar === next.settings.permanentTopBar && prev.settings.buttonControlBar === next.settings.buttonControlBar && prev.settings.aspectRatio === next.settings.aspectRatio && diff --git a/app/src/components/MeetingViews/Filmstrip.js b/app/src/components/MeetingViews/Filmstrip.js index 9bfcaabd..58e9c557 100644 --- a/app/src/components/MeetingViews/Filmstrip.js +++ b/app/src/components/MeetingViews/Filmstrip.js @@ -407,6 +407,7 @@ export default withRoomContext(connect( prev.room.selectedPeers === next.room.selectedPeers && prev.room.toolbarsVisible === next.room.toolbarsVisible && prev.room.hideSelfView === next.room.hideSelfView && + prev.room.iframeUrl === next.room.iframeUrl && prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen && prev.settings.permanentTopBar === next.settings.permanentTopBar && prev.settings.aspectRatio === next.settings.aspectRatio && diff --git a/app/src/components/VideoContainers/VideoView.js b/app/src/components/VideoContainers/VideoView.js index 7d7f3969..b02c3cdc 100644 --- a/app/src/components/VideoContainers/VideoView.js +++ b/app/src/components/VideoContainers/VideoView.js @@ -10,6 +10,10 @@ import SignalCellular0BarIcon from '@material-ui/icons/SignalCellular0Bar'; import SignalCellular1BarIcon from '@material-ui/icons/SignalCellular1Bar'; import SignalCellular2BarIcon from '@material-ui/icons/SignalCellular2Bar'; import SignalCellular3BarIcon from '@material-ui/icons/SignalCellular3Bar'; +import Iframe from 'react-iframe'; +import IconButton from '@material-ui/core/IconButton'; +import FullscreenIcon from '@material-ui/icons/Fullscreen'; +import ReplayIcon from '@material-ui/icons/Replay'; import { AudioAnalyzer } from './AudioAnalyzer'; const logger = new Logger('VideoView'); @@ -172,6 +176,27 @@ const styles = (theme) => { backgroundColor : 'rgb(174, 255, 0, 0.25)' } + }, + iframeContainer : + { + display : 'flex', + flexFlow : 'column', + height : '100%', + background : 'white' + }, + iframeMenu : + { + background : 'white' + }, + iframeMain : + { + flex : 1, + overflow : 'auto', + border : 0 + }, + iframeButtons : + { + padding : theme.spacing(1) } }); @@ -206,6 +231,8 @@ class VideoView extends React.PureComponent { const { isMe, + isIframe, + iframeUrl, isMirrored, isScreen, isExtraVideo, @@ -305,6 +332,7 @@ class VideoView extends React.PureComponent return (
+ { !isIframe &&
{(audioCodec || videoCodec) && @@ -448,7 +476,8 @@ class VideoView extends React.PureComponent
}
- + } + {!isIframe &&