Merge remote-tracking branch 'upstream/master' into chat-old-messages-re-render
This commit is contained in:
commit
8ecab69821
13
README.md
13
README.md
@ -2,15 +2,18 @@ BigBlueButton
|
||||
=============
|
||||
BigBlueButton is an open source web conferencing system.
|
||||
|
||||
BigBlueButton supports real-time sharing of audio, video, slides (with whiteboard controls), chat, and the screen. Instructors can engage remote students with polling, emojis, and breakout rooms. BigBlueButton can record and playback all content shared in a session.
|
||||
BigBlueButton supports real-time sharing of audio, video, slides (with whiteboard controls), chat, and the screen. Instructors can engage remote students with polling, emojis, multi-user whiteboard, and breakout rooms.
|
||||
|
||||
Presenters can record and playback content for later sharing with others.
|
||||
|
||||
We designed BigBlueButton for online learning (though it can be used for many [other applications](http://www.c4isrnet.com/story/military-tech/disa/2015/02/11/disa-to-save-12m-defense-collaboration-services/23238997/)). The educational use cases for BigBlueButton are
|
||||
|
||||
* One-to-one on-line tutoring
|
||||
* Small group collaboration
|
||||
* On-line classes
|
||||
* Online tutoring (one-to-one)
|
||||
* Flipped classrooms (recording content ahead of your session)
|
||||
* Group collaboration (many-to-many)
|
||||
* Online classes (one-to-many)
|
||||
|
||||
BigBlueButton runs on a Ubuntu 16.04 64-bit server. If you follow the [installation instructions](http://docs.bigbluebutton.org/install/install.html), we guarantee you will have BigBlueButton installed and running within 30 minutes (or your money back :-).
|
||||
You can install on a Ubuntu 16.04 64-bit server. We provide [bbb-install.sh](https://github.com/bigbluebutton/bbb-install) to let you have a server up and running within 30 minutes (or your money back :-).
|
||||
|
||||
For full technical documentation BigBlueButton -- including architecture, features, API, and GreenLight (the default front-end) -- see [http://docs.bigbluebutton.org/](http://docs.bigbluebutton.org/).
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
font-family: 'Source Sans Pro', Arial, sans-serif;
|
||||
font-size: 1rem; /* 16px */
|
||||
|
@ -8,10 +8,15 @@ function breakouts(credentials, moderator) {
|
||||
requesterUserId,
|
||||
} = credentials;
|
||||
Logger.info(`Publishing Breakouts for ${meetingId} ${requesterUserId}`);
|
||||
|
||||
if (moderator) {
|
||||
const presenterSelector = {
|
||||
parentMeetingId: meetingId,
|
||||
$or: [
|
||||
{ parentMeetingId: meetingId },
|
||||
{ breakoutId: meetingId },
|
||||
],
|
||||
};
|
||||
|
||||
return Breakouts.find(presenterSelector);
|
||||
}
|
||||
|
||||
|
@ -47,9 +47,9 @@ class Base extends Component {
|
||||
|
||||
componentWillUpdate() {
|
||||
const { approved } = this.props;
|
||||
const isLoading = this.state.loading;
|
||||
const { loading } = this.state;
|
||||
|
||||
if (approved && isLoading) this.updateLoadingState(false);
|
||||
if (approved && loading) this.updateLoadingState(false);
|
||||
}
|
||||
|
||||
updateLoadingState(loading = false) {
|
||||
@ -131,7 +131,8 @@ const BaseContainer = withTracker(() => {
|
||||
},
|
||||
};
|
||||
|
||||
const subscriptionsHandlers = SUBSCRIPTIONS_NAME.map(name => Meteor.subscribe(name, credentials, subscriptionErrorHandler));
|
||||
const subscriptionsHandlers = SUBSCRIPTIONS_NAME
|
||||
.map(name => Meteor.subscribe(name, credentials, subscriptionErrorHandler));
|
||||
|
||||
const chats = GroupChat.find({
|
||||
$or: [
|
||||
@ -147,7 +148,8 @@ const BaseContainer = withTracker(() => {
|
||||
const chatIds = chats.map(chat => chat.chatId);
|
||||
|
||||
const groupChatMessageHandler = Meteor.subscribe('group-chat-msg', credentials, chatIds, subscriptionErrorHandler);
|
||||
const User = Users.findOne({ intId: credentials.externUserID });
|
||||
const User = Users.findOne({ intId: credentials.requesterUserId });
|
||||
|
||||
if (User) {
|
||||
const mappedUser = mapUser(User);
|
||||
breakoutRoomSubscriptionHandler = Meteor.subscribe('breakouts', credentials, mappedUser.isModerator, subscriptionErrorHandler);
|
||||
|
@ -20,6 +20,17 @@ const propTypes = {
|
||||
isUserPresenter: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
isUserModerator: PropTypes.bool.isRequired,
|
||||
allowStartStopRecording: PropTypes.bool.isRequired,
|
||||
isRecording: PropTypes.bool.isRequired,
|
||||
record: PropTypes.func.isRequired,
|
||||
toggleRecording: PropTypes.func.isRequired,
|
||||
meetingIsBreakout: PropTypes.bool.isRequired,
|
||||
hasBreakoutRoom: PropTypes.bool.isRequired,
|
||||
createBreakoutRoom: PropTypes.func.isRequired,
|
||||
meetingName: PropTypes.string.isRequired,
|
||||
shortcuts: PropTypes.string.isRequired,
|
||||
users: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -116,20 +127,37 @@ class ActionsDropdown extends Component {
|
||||
isRecording,
|
||||
record,
|
||||
toggleRecording,
|
||||
togglePollMenu,
|
||||
meetingIsBreakout,
|
||||
hasBreakoutRoom,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
pollBtnLabel,
|
||||
pollBtnDesc,
|
||||
presentationLabel,
|
||||
presentationDesc,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
createBreakoutRoom,
|
||||
createBreakoutRoomDesc,
|
||||
} = intlMessages;
|
||||
|
||||
const {
|
||||
formatMessage,
|
||||
} = intl;
|
||||
|
||||
return _.compact([
|
||||
(isUserPresenter
|
||||
? (
|
||||
<DropdownListItem
|
||||
icon="user"
|
||||
label={intl.formatMessage(intlMessages.pollBtnLabel)}
|
||||
description={intl.formatMessage(intlMessages.pollBtnDesc)}
|
||||
label={formatMessage(pollBtnLabel)}
|
||||
description={formatMessage(pollBtnDesc)}
|
||||
key={this.pollId}
|
||||
onClick={() => togglePollMenu()}
|
||||
onClick={() => {
|
||||
Session.set('openPanel', 'poll');
|
||||
Session.set('forcePollOpen', true);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: null),
|
||||
@ -138,8 +166,8 @@ class ActionsDropdown extends Component {
|
||||
<DropdownListItem
|
||||
data-test="uploadPresentation"
|
||||
icon="presentation"
|
||||
label={intl.formatMessage(intlMessages.presentationLabel)}
|
||||
description={intl.formatMessage(intlMessages.presentationDesc)}
|
||||
label={formatMessage(presentationLabel)}
|
||||
description={formatMessage(presentationDesc)}
|
||||
key={this.presentationItemId}
|
||||
onClick={this.handlePresentationClick}
|
||||
/>
|
||||
@ -149,10 +177,10 @@ class ActionsDropdown extends Component {
|
||||
? (
|
||||
<DropdownListItem
|
||||
icon="record"
|
||||
label={intl.formatMessage(isRecording
|
||||
? intlMessages.stopRecording : intlMessages.startRecording)}
|
||||
description={intl.formatMessage(isRecording
|
||||
? intlMessages.stopRecording : intlMessages.startRecording)}
|
||||
label={formatMessage(isRecording
|
||||
? stopRecording : startRecording)}
|
||||
description={formatMessage(isRecording
|
||||
? stopRecording : startRecording)}
|
||||
key={this.recordId}
|
||||
onClick={toggleRecording}
|
||||
/>
|
||||
@ -162,8 +190,8 @@ class ActionsDropdown extends Component {
|
||||
? (
|
||||
<DropdownListItem
|
||||
icon="rooms"
|
||||
label={intl.formatMessage(intlMessages.createBreakoutRoom)}
|
||||
description={intl.formatMessage(intlMessages.createBreakoutRoomDesc)}
|
||||
label={formatMessage(createBreakoutRoom)}
|
||||
description={formatMessage(createBreakoutRoomDesc)}
|
||||
key={this.createBreakoutRoomId}
|
||||
onClick={this.handleCreateBreakoutRoomClick}
|
||||
/>
|
||||
@ -173,7 +201,8 @@ class ActionsDropdown extends Component {
|
||||
}
|
||||
|
||||
handlePresentationClick() {
|
||||
this.props.mountModal(<PresentationUploaderContainer />);
|
||||
const { mountModal } = this.props;
|
||||
mountModal(<PresentationUploaderContainer />);
|
||||
}
|
||||
|
||||
handleCreateBreakoutRoomClick() {
|
||||
@ -189,7 +218,8 @@ class ActionsDropdown extends Component {
|
||||
createBreakoutRoom={createBreakoutRoom}
|
||||
meetingName={meetingName}
|
||||
users={users}
|
||||
/>);
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -18,7 +18,6 @@ class ActionsBar extends React.PureComponent {
|
||||
isUserModerator,
|
||||
recordSettingsList,
|
||||
toggleRecording,
|
||||
togglePollMenu,
|
||||
screenSharingCheck,
|
||||
enableVideo,
|
||||
createBreakoutRoom,
|
||||
@ -48,7 +47,6 @@ class ActionsBar extends React.PureComponent {
|
||||
isRecording,
|
||||
record,
|
||||
toggleRecording,
|
||||
togglePollMenu,
|
||||
createBreakoutRoom,
|
||||
meetingIsBreakout,
|
||||
hasBreakoutRoom,
|
||||
@ -57,21 +55,27 @@ class ActionsBar extends React.PureComponent {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={isUserPresenter ? cx(styles.centerWithActions, actionBarClasses) : styles.center}>
|
||||
<div
|
||||
className={
|
||||
isUserPresenter ? cx(styles.centerWithActions, actionBarClasses) : styles.center
|
||||
}
|
||||
>
|
||||
<AudioControlsContainer />
|
||||
{enableVideo ?
|
||||
<JoinVideoOptionsContainer
|
||||
handleJoinVideo={handleJoinVideo}
|
||||
handleCloseVideo={handleExitVideo}
|
||||
/>
|
||||
{enableVideo
|
||||
? (
|
||||
<JoinVideoOptionsContainer
|
||||
handleJoinVideo={handleJoinVideo}
|
||||
handleCloseVideo={handleExitVideo}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
<DesktopShare {...{
|
||||
handleShareScreen,
|
||||
handleUnshareScreen,
|
||||
isVideoBroadcasting,
|
||||
isUserPresenter,
|
||||
screenSharingCheck,
|
||||
}}
|
||||
handleShareScreen,
|
||||
handleUnshareScreen,
|
||||
isVideoBroadcasting,
|
||||
isUserPresenter,
|
||||
screenSharingCheck,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { Session } from 'meteor/session';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
@ -12,23 +11,6 @@ import { shareScreen, unshareScreen, isVideoBroadcasting } from '../screenshare/
|
||||
const ActionsBarContainer = props => <ActionsBar {...props} />;
|
||||
|
||||
export default withTracker(() => {
|
||||
const togglePollMenu = () => {
|
||||
const showPoll = Session.equals('isPollOpen', false) || !Session.get('isPollOpen');
|
||||
|
||||
const show = () => {
|
||||
Session.set('isUserListOpen', true);
|
||||
Session.set('isPollOpen', true);
|
||||
Session.set('forcePollOpen', true);
|
||||
};
|
||||
|
||||
const hide = () => Session.set('isPollOpen', false);
|
||||
|
||||
Session.set('isChatOpen', false);
|
||||
Session.set('breakoutRoomIsOpen', false);
|
||||
|
||||
return showPoll ? show() : hide();
|
||||
};
|
||||
|
||||
Meetings.find({ meetingId: Auth.meetingID }).observeChanges({
|
||||
changed: (id, fields) => {
|
||||
if (fields.recordProp && fields.recordProp.recording) {
|
||||
@ -57,7 +39,6 @@ export default withTracker(() => {
|
||||
meetingIsBreakout: Service.meetingIsBreakout(),
|
||||
hasBreakoutRoom: Service.hasBreakoutRoom(),
|
||||
meetingName: Service.meetingName(),
|
||||
togglePollMenu,
|
||||
users: Service.users(),
|
||||
};
|
||||
})(ActionsBarContainer);
|
||||
|
@ -3,12 +3,9 @@ import PropTypes from 'prop-types';
|
||||
import { throttle } from 'lodash';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import Modal from 'react-modal';
|
||||
import cx from 'classnames';
|
||||
import Resizable from 're-resizable';
|
||||
import browser from 'browser-detect';
|
||||
import BreakoutRoomContainer from '/imports/ui/components/breakout-room/container';
|
||||
import PanelManager from '/imports/ui/components/panel-manager/component';
|
||||
import PollingContainer from '/imports/ui/components/polling/container';
|
||||
import PollContainer from '/imports/ui/components/poll/container';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import ToastContainer from '../toast/container';
|
||||
import ModalContainer from '../modal/container';
|
||||
@ -16,12 +13,8 @@ import NotificationsBarContainer from '../notifications-bar/container';
|
||||
import AudioContainer from '../audio/container';
|
||||
import ChatAlertContainer from '../chat/alert/container';
|
||||
import { styles } from './styles';
|
||||
import UserListContainer from '../user-list/container';
|
||||
import ChatContainer from '../chat/container';
|
||||
|
||||
|
||||
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
||||
const USERLIST_COMPACT_WIDTH = 50;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
userListLabel: {
|
||||
@ -51,7 +44,6 @@ const propTypes = {
|
||||
closedCaption: PropTypes.element,
|
||||
userListIsOpen: PropTypes.bool.isRequired,
|
||||
chatIsOpen: PropTypes.bool.isRequired,
|
||||
pollIsOpen: PropTypes.bool.isRequired,
|
||||
locale: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
@ -71,7 +63,6 @@ class App extends Component {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
compactUserList: false,
|
||||
enableResize: !window.matchMedia(MOBILE_MEDIA).matches,
|
||||
};
|
||||
|
||||
@ -79,11 +70,11 @@ class App extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { locale } = this.props;
|
||||
const { locale, fontSize } = this.props;
|
||||
|
||||
Modal.setAppElement('#app');
|
||||
document.getElementsByTagName('html')[0].lang = locale;
|
||||
document.getElementsByTagName('html')[0].style.fontSize = this.props.fontSize;
|
||||
document.getElementsByTagName('html')[0].style.fontSize = fontSize;
|
||||
|
||||
const BROWSER_RESULTS = browser();
|
||||
const body = document.getElementsByTagName('body')[0];
|
||||
@ -112,15 +103,17 @@ class App extends Component {
|
||||
this.setState({ enableResize: shouldEnableResize });
|
||||
}
|
||||
|
||||
renderPoll() {
|
||||
const { pollIsOpen } = this.props;
|
||||
|
||||
if (!pollIsOpen) return null;
|
||||
renderPanel() {
|
||||
const { enableResize } = this.state;
|
||||
const { openPanel } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.poll}>
|
||||
<PollContainer />
|
||||
</div>
|
||||
<PanelManager
|
||||
{...{
|
||||
openPanel,
|
||||
enableResize,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -160,136 +153,6 @@ class App extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderUserList() {
|
||||
const {
|
||||
intl, chatIsOpen, userListIsOpen,
|
||||
} = this.props;
|
||||
|
||||
const { compactUserList } = this.state;
|
||||
|
||||
if (!userListIsOpen) return null;
|
||||
|
||||
const userListStyle = {};
|
||||
userListStyle[styles.compact] = compactUserList;
|
||||
// userList = React.cloneElement(userList, {
|
||||
// compact: compactUserList, // TODO 4767
|
||||
// });
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.userList, userListStyle)}
|
||||
aria-label={intl.formatMessage(intlMessages.userListLabel)}
|
||||
aria-hidden={chatIsOpen}
|
||||
>
|
||||
<UserListContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderBreakoutRoom() {
|
||||
const { hasBreakoutRooms, breakoutRoomIsOpen } = this.props;
|
||||
|
||||
if (!breakoutRoomIsOpen) return null;
|
||||
if (!hasBreakoutRooms) return null;
|
||||
return (
|
||||
<div className={styles.breakoutRoom}>
|
||||
<BreakoutRoomContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderUserListResizable() {
|
||||
const { userListIsOpen } = this.props;
|
||||
|
||||
// Variables for resizing user-list.
|
||||
const USERLIST_MIN_WIDTH_PX = 150;
|
||||
const USERLIST_MAX_WIDTH_PX = 240;
|
||||
const USERLIST_DEFAULT_WIDTH_RELATIVE = 18;
|
||||
|
||||
// decide whether using pixel or percentage unit as a default width for userList
|
||||
const USERLIST_DEFAULT_WIDTH = (window.innerWidth * (USERLIST_DEFAULT_WIDTH_RELATIVE / 100.0)) < USERLIST_MAX_WIDTH_PX ? `${USERLIST_DEFAULT_WIDTH_RELATIVE}%` : USERLIST_MAX_WIDTH_PX;
|
||||
|
||||
if (!userListIsOpen) return null;
|
||||
|
||||
const resizableEnableOptions = {
|
||||
top: false,
|
||||
right: true,
|
||||
bottom: false,
|
||||
left: false,
|
||||
topRight: false,
|
||||
bottomRight: false,
|
||||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
defaultSize={{ width: USERLIST_DEFAULT_WIDTH }}
|
||||
minWidth={USERLIST_MIN_WIDTH_PX}
|
||||
maxWidth={USERLIST_MAX_WIDTH_PX}
|
||||
ref={(node) => { this.resizableUserList = node; }}
|
||||
enable={resizableEnableOptions}
|
||||
onResize={(e, direction, ref) => {
|
||||
const { compactUserList } = this.state;
|
||||
const shouldBeCompact = ref.clientWidth <= USERLIST_COMPACT_WIDTH;
|
||||
if (compactUserList === shouldBeCompact) return;
|
||||
this.setState({ compactUserList: shouldBeCompact });
|
||||
}}
|
||||
>
|
||||
{this.renderUserList()}
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
|
||||
renderChat() {
|
||||
const { intl, chatIsOpen } = this.props;
|
||||
|
||||
if (!chatIsOpen) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={styles.chat}
|
||||
aria-label={intl.formatMessage(intlMessages.chatLabel)}
|
||||
>
|
||||
<ChatContainer />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderChatResizable() {
|
||||
const { chatIsOpen } = this.props;
|
||||
|
||||
// Variables for resizing chat.
|
||||
const CHAT_MIN_WIDTH = '10%';
|
||||
const CHAT_MAX_WIDTH = '25%';
|
||||
const CHAT_DEFAULT_WIDTH = '15%';
|
||||
|
||||
if (!chatIsOpen) return null;
|
||||
|
||||
const resizableEnableOptions = {
|
||||
top: false,
|
||||
right: true,
|
||||
bottom: false,
|
||||
left: false,
|
||||
topRight: false,
|
||||
bottomRight: false,
|
||||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
defaultSize={{ width: CHAT_DEFAULT_WIDTH }}
|
||||
minWidth={CHAT_MIN_WIDTH}
|
||||
maxWidth={CHAT_MAX_WIDTH}
|
||||
ref={(node) => { this.resizableChat = node; }}
|
||||
enable={resizableEnableOptions}
|
||||
>
|
||||
{this.renderChat()}
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
|
||||
renderMedia() {
|
||||
const {
|
||||
media, intl, chatIsOpen, userListIsOpen,
|
||||
@ -329,9 +192,8 @@ class App extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
userListIsOpen, customStyle, customStyleUrl, micsLocked,
|
||||
customStyle, customStyleUrl, micsLocked,
|
||||
} = this.props;
|
||||
const { enableResize } = this.state;
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
@ -342,11 +204,7 @@ class App extends Component {
|
||||
{this.renderMedia()}
|
||||
{this.renderActionsBar()}
|
||||
</div>
|
||||
{enableResize ? this.renderUserListResizable() : this.renderUserList()}
|
||||
{userListIsOpen && enableResize ? <div className={styles.userlistPad} /> : null}
|
||||
{enableResize ? this.renderChatResizable() : this.renderChat()}
|
||||
{this.renderPoll()}
|
||||
{this.renderBreakoutRoom()}
|
||||
{this.renderPanel()}
|
||||
{this.renderSidebar()}
|
||||
</section>
|
||||
<PollingContainer />
|
||||
|
@ -67,7 +67,6 @@ const AppContainer = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) => {
|
||||
const currentUser = Users.findOne({ userId: Auth.userID });
|
||||
const currentUserIsLocked = mapUser(currentUser).isLocked;
|
||||
@ -111,12 +110,12 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
closedCaption: getCaptionsStatus() ? <ClosedCaptionsContainer /> : null,
|
||||
fontSize: getFontSize(),
|
||||
hasBreakoutRooms: getBreakoutRooms().length > 0,
|
||||
userListIsOpen: Session.get('isUserListOpen'),
|
||||
breakoutRoomIsOpen: Session.get('breakoutRoomIsOpen') && Session.get('isUserListOpen'),
|
||||
chatIsOpen: Session.get('isChatOpen') && Session.get('isUserListOpen'),
|
||||
pollIsOpen: Session.get('isPollOpen') && Session.get('isUserListOpen'),
|
||||
customStyle: getFromUserSettings('customStyle', false),
|
||||
customStyleUrl: getFromUserSettings('customStyleUrl', false),
|
||||
breakoutRoomIsOpen: Session.equals('openPanel', 'breakoutroom'),
|
||||
chatIsOpen: Session.equals('openPanel', 'chat'),
|
||||
openPanel: Session.get('openPanel'),
|
||||
userListIsOpen: !Session.equals('openPanel', ''),
|
||||
micsLocked: (currentUserIsLocked && meeting.lockSettingsProp.disableMic),
|
||||
};
|
||||
})(AppContainer)));
|
||||
|
@ -12,9 +12,8 @@
|
||||
}
|
||||
|
||||
.main {
|
||||
position: fixed;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -25,9 +25,11 @@ const breakoutRoomUser = (breakoutId) => {
|
||||
return breakoutUser;
|
||||
};
|
||||
|
||||
const closeBreakoutPanel = () => Session.set('openPanel', 'userlist');
|
||||
|
||||
const endAllBreakouts = () => {
|
||||
makeCall('endAllBreakouts');
|
||||
closeBreakoutPanel();
|
||||
};
|
||||
|
||||
const requestJoinURL = (breakoutId) => {
|
||||
@ -63,9 +65,6 @@ const isModerator = () => {
|
||||
return mappedUser.isModerator;
|
||||
};
|
||||
|
||||
|
||||
const closeBreakoutPanel = () => Session.set('breakoutRoomIsOpen', false);
|
||||
|
||||
export default {
|
||||
findBreakouts,
|
||||
endAllBreakouts,
|
||||
|
@ -23,10 +23,9 @@ class ChatPushAlert extends PureComponent {
|
||||
aria-label={message}
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
Session.set('isUserListOpen', true);
|
||||
Session.set('isChatOpen', true);
|
||||
Session.set('idChatOpen', chatId);
|
||||
}}
|
||||
Session.set('openPanel', 'chat');
|
||||
Session.set('idChatOpen', chatId);
|
||||
}}
|
||||
>
|
||||
{ message }
|
||||
</div>
|
||||
|
@ -56,7 +56,7 @@ const Chat = (props) => {
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
Session.set('isChatOpen', false);
|
||||
Session.set('openPanel', 'userlist');
|
||||
}}
|
||||
aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })}
|
||||
accessKey={HIDE_CHAT_AK}
|
||||
@ -66,23 +66,23 @@ const Chat = (props) => {
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
chatID !== 'public' ?
|
||||
<Button
|
||||
className={styles.closeBtn}
|
||||
icon="close"
|
||||
size="md"
|
||||
hideLabel
|
||||
onClick={() => {
|
||||
actions.handleClosePrivateChat(chatID);
|
||||
Session.set('isChatOpen', false);
|
||||
Session.set('idChatOpen', '');
|
||||
}}
|
||||
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
|
||||
label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
|
||||
accessKey={CLOSE_CHAT_AK}
|
||||
/>
|
||||
:
|
||||
<ChatDropdown />
|
||||
chatID !== 'public'
|
||||
? (
|
||||
<Button
|
||||
className={styles.closeBtn}
|
||||
icon="close"
|
||||
size="md"
|
||||
hideLabel
|
||||
onClick={() => {
|
||||
actions.handleClosePrivateChat(chatID);
|
||||
Session.set('openPanel', 'userlist');
|
||||
}}
|
||||
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
|
||||
label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
|
||||
accessKey={CLOSE_CHAT_AK}
|
||||
/>
|
||||
)
|
||||
: <ChatDropdown />
|
||||
}
|
||||
</header>
|
||||
<MessageList
|
||||
@ -122,6 +122,7 @@ const propTypes = {
|
||||
PropTypes.object,
|
||||
])).isRequired).isRequired,
|
||||
scrollPosition: PropTypes.number,
|
||||
shortcuts: PropTypes.string.isRequired,
|
||||
hasUnreadMessages: PropTypes.bool.isRequired,
|
||||
lastReadMessageTime: PropTypes.number.isRequired,
|
||||
partnerIsLoggedOut: PropTypes.bool.isRequired,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Session } from 'meteor/session';
|
||||
import PropTypes from 'prop-types';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { setCustomLogoUrl } from '/imports/ui/components/user-list/service';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
@ -7,6 +8,9 @@ import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import LoadingScreen from '/imports/ui/components/loading-screen/component';
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
class JoinHandler extends Component {
|
||||
static setError(codeError) {
|
||||
@ -99,7 +103,6 @@ class JoinHandler extends Component {
|
||||
};
|
||||
// use enter api to get params for the client
|
||||
const url = `/bigbluebutton/api/enter?sessionToken=${sessionToken}`;
|
||||
|
||||
const fetchContent = await fetch(url, { credentials: 'same-origin' });
|
||||
const parseToJson = await fetchContent.json();
|
||||
const { response } = parseToJson;
|
||||
@ -109,7 +112,11 @@ class JoinHandler extends Component {
|
||||
await setCustomData(response);
|
||||
setLogoURL(response);
|
||||
logUserInfo();
|
||||
Session.set('isUserListOpen', deviceInfo.type().isPhone);
|
||||
|
||||
Session.set('openPanel', 'chat');
|
||||
Session.set('idChatOpen', '');
|
||||
if (deviceInfo.type().isPhone) Session.set('openPanel', '');
|
||||
|
||||
logger.info(`User successfully went through main.joinRouteHandler with [${JSON.stringify(response)}].`);
|
||||
} else {
|
||||
const e = new Error('Session not found');
|
||||
@ -128,3 +135,5 @@ class JoinHandler extends Component {
|
||||
}
|
||||
|
||||
export default JoinHandler;
|
||||
|
||||
JoinHandler.propTypes = propTypes;
|
||||
|
@ -24,7 +24,7 @@ export default class Media extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
swapLayout, floatingOverlay, hideOverlay, disableVideo,
|
||||
swapLayout, floatingOverlay, hideOverlay, disableVideo, children,
|
||||
} = this.props;
|
||||
|
||||
const contentClassName = cx({
|
||||
@ -40,7 +40,7 @@ export default class Media extends Component {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={!swapLayout ? contentClassName : overlayClassName}>
|
||||
{this.props.children}
|
||||
{children}
|
||||
</div>
|
||||
<div className={!swapLayout ? overlayClassName : contentClassName}>
|
||||
{ !disableVideo ? <VideoProviderContainer /> : null }
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Session } from 'meteor/session';
|
||||
import _ from 'lodash';
|
||||
import cx from 'classnames';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
@ -48,7 +49,7 @@ const intlMessages = defineMessages({
|
||||
const propTypes = {
|
||||
presentationTitle: PropTypes.string,
|
||||
hasUnreadMessages: PropTypes.bool,
|
||||
beingRecorded: PropTypes.object,
|
||||
beingRecorded: PropTypes.bool,
|
||||
shortcuts: PropTypes.string,
|
||||
};
|
||||
|
||||
@ -59,10 +60,12 @@ const defaultProps = {
|
||||
shortcuts: '',
|
||||
};
|
||||
|
||||
const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mountModal(<BreakoutJoinConfirmation
|
||||
breakout={breakout}
|
||||
breakoutName={breakoutName}
|
||||
/>);
|
||||
const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mountModal(
|
||||
<BreakoutJoinConfirmation
|
||||
breakout={breakout}
|
||||
breakoutName={breakoutName}
|
||||
/>,
|
||||
);
|
||||
|
||||
const closeBreakoutJoinConfirmation = mountModal => mountModal(null);
|
||||
|
||||
@ -85,6 +88,10 @@ class NavBar extends PureComponent {
|
||||
mountModal,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
didSendBreakoutInvite,
|
||||
} = this.state;
|
||||
|
||||
const hadBreakouts = oldProps.breakouts.length;
|
||||
const hasBreakouts = breakouts.length;
|
||||
|
||||
@ -101,18 +108,23 @@ class NavBar extends PureComponent {
|
||||
|
||||
if (!userOnMeeting) return;
|
||||
|
||||
if (!this.state.didSendBreakoutInvite && !isBreakoutRoom) {
|
||||
if (!didSendBreakoutInvite && !isBreakoutRoom) {
|
||||
this.inviteUserToBreakout(breakout);
|
||||
}
|
||||
});
|
||||
|
||||
if (!breakouts.length && this.state.didSendBreakoutInvite) {
|
||||
if (!breakouts.length && didSendBreakoutInvite) {
|
||||
this.setState({ didSendBreakoutInvite: false });
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleUserList() {
|
||||
this.props.toggleUserList();
|
||||
Session.set(
|
||||
'openPanel',
|
||||
Session.get('openPanel') !== ''
|
||||
? ''
|
||||
: 'userlist',
|
||||
);
|
||||
}
|
||||
|
||||
inviteUserToBreakout(breakout) {
|
||||
@ -132,6 +144,10 @@ class NavBar extends PureComponent {
|
||||
presentationTitle,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isActionsOpen,
|
||||
} = this.state;
|
||||
|
||||
if (isBreakoutRoom || !breakouts.length) {
|
||||
return (
|
||||
<h1 className={styles.presentationTitle}>{presentationTitle}</h1>
|
||||
@ -140,7 +156,7 @@ class NavBar extends PureComponent {
|
||||
const breakoutItems = breakouts.map(breakout => this.renderBreakoutItem(breakout));
|
||||
|
||||
return (
|
||||
<Dropdown isOpen={this.state.isActionsOpen}>
|
||||
<Dropdown isOpen={isActionsOpen}>
|
||||
<DropdownTrigger>
|
||||
<h1 className={cx(styles.presentationTitle, styles.dropdownBreakout)}>
|
||||
{presentationTitle}
|
||||
@ -170,7 +186,9 @@ class NavBar extends PureComponent {
|
||||
<DropdownListItem
|
||||
key={_.uniqueId('action-header')}
|
||||
label={breakoutName}
|
||||
onClick={openBreakoutJoinConfirmation.bind(this, breakout, breakoutName, mountModal)}
|
||||
onClick={
|
||||
openBreakoutJoinConfirmation.bind(this, breakout, breakoutName, mountModal)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,198 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import BreakoutRoomContainer from '/imports/ui/components/breakout-room/container';
|
||||
import UserListContainer from '/imports/ui/components/user-list/container';
|
||||
import ChatContainer from '/imports/ui/components/chat/container';
|
||||
import PollContainer from '/imports/ui/components/poll/container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Resizable from 're-resizable';
|
||||
import { styles } from '/imports/ui/components/app/styles';
|
||||
import _ from 'lodash';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
chatLabel: {
|
||||
id: 'app.chat.label',
|
||||
description: 'Aria-label for Chat Section',
|
||||
},
|
||||
userListLabel: {
|
||||
id: 'app.userList.label',
|
||||
description: 'Aria-label for Userlist Nav',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
enableResize: PropTypes.bool.isRequired,
|
||||
openPanel: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
class PanelManager extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.padKey = _.uniqueId('resize-pad-');
|
||||
this.userlistKey = _.uniqueId('userlist-');
|
||||
this.breakoutroomKey = _.uniqueId('breakoutroom-');
|
||||
this.chatKey = _.uniqueId('chat-');
|
||||
this.pollKey = _.uniqueId('poll-');
|
||||
}
|
||||
|
||||
renderUserList() {
|
||||
const {
|
||||
intl,
|
||||
enableResize,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.userList}
|
||||
aria-label={intl.formatMessage(intlMessages.userListLabel)}
|
||||
key={enableResize ? null : this.userlistKey}
|
||||
>
|
||||
<UserListContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderUserListResizable() {
|
||||
// Variables for resizing user-list.
|
||||
const USERLIST_MIN_WIDTH_PX = 150;
|
||||
const USERLIST_MAX_WIDTH_PX = 240;
|
||||
const USERLIST_DEFAULT_WIDTH_RELATIVE = 18;
|
||||
|
||||
// decide whether using pixel or percentage unit as a default width for userList
|
||||
const USERLIST_DEFAULT_WIDTH = (window.innerWidth * (USERLIST_DEFAULT_WIDTH_RELATIVE / 100.0)) < USERLIST_MAX_WIDTH_PX ? `${USERLIST_DEFAULT_WIDTH_RELATIVE}%` : USERLIST_MAX_WIDTH_PX;
|
||||
|
||||
const resizableEnableOptions = {
|
||||
top: false,
|
||||
right: true,
|
||||
bottom: false,
|
||||
left: false,
|
||||
topRight: false,
|
||||
bottomRight: false,
|
||||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
defaultSize={{ width: USERLIST_DEFAULT_WIDTH }}
|
||||
minWidth={USERLIST_MIN_WIDTH_PX}
|
||||
maxWidth={USERLIST_MAX_WIDTH_PX}
|
||||
ref={(node) => { this.resizableUserList = node; }}
|
||||
className={styles.resizableUserList}
|
||||
enable={resizableEnableOptions}
|
||||
key={this.userlistKey}
|
||||
>
|
||||
{this.renderUserList()}
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
|
||||
renderChat() {
|
||||
const { intl, enableResize } = this.props;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={styles.chat}
|
||||
aria-label={intl.formatMessage(intlMessages.chatLabel)}
|
||||
key={enableResize ? null : this.chatKey}
|
||||
>
|
||||
<ChatContainer />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderChatResizable() {
|
||||
// Variables for resizing chat.
|
||||
const CHAT_MIN_WIDTH = '10%';
|
||||
const CHAT_MAX_WIDTH = '25%';
|
||||
const CHAT_DEFAULT_WIDTH = '15%';
|
||||
|
||||
const resizableEnableOptions = {
|
||||
top: false,
|
||||
right: true,
|
||||
bottom: false,
|
||||
left: false,
|
||||
topRight: false,
|
||||
bottomRight: false,
|
||||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
defaultSize={{ width: CHAT_DEFAULT_WIDTH }}
|
||||
minWidth={CHAT_MIN_WIDTH}
|
||||
maxWidth={CHAT_MAX_WIDTH}
|
||||
ref={(node) => { this.resizableChat = node; }}
|
||||
className={styles.resizableChat}
|
||||
enable={resizableEnableOptions}
|
||||
key={this.chatKey}
|
||||
>
|
||||
{this.renderChat()}
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
|
||||
renderPoll() {
|
||||
return (
|
||||
<div className={styles.poll} key={this.pollKey}>
|
||||
<PollContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderBreakoutRoom() {
|
||||
return (
|
||||
<div className={styles.breakoutRoom} key={this.breakoutroomKey}>
|
||||
<BreakoutRoomContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { enableResize, openPanel } = this.props;
|
||||
if (openPanel === '') return null;
|
||||
|
||||
const panels = [this.renderUserList()];
|
||||
const resizablePanels = [
|
||||
this.renderUserListResizable(),
|
||||
<div className={styles.userlistPad} key={this.padKey} />,
|
||||
];
|
||||
|
||||
if (openPanel === 'chat') {
|
||||
if (enableResize) {
|
||||
resizablePanels.push(this.renderChatResizable());
|
||||
} else {
|
||||
panels.push(this.renderChat());
|
||||
}
|
||||
}
|
||||
|
||||
if (openPanel === 'poll') {
|
||||
if (enableResize) {
|
||||
resizablePanels.push(this.renderPoll());
|
||||
} else {
|
||||
panels.push(this.renderPoll());
|
||||
}
|
||||
}
|
||||
|
||||
if (openPanel === 'breakoutroom') {
|
||||
if (enableResize) {
|
||||
resizablePanels.push(this.renderBreakoutRoom());
|
||||
} else {
|
||||
panels.push(this.renderBreakoutRoom());
|
||||
}
|
||||
}
|
||||
|
||||
return enableResize
|
||||
? resizablePanels
|
||||
: panels;
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(PanelManager);
|
||||
|
||||
PanelManager.propTypes = propTypes;
|
@ -92,6 +92,16 @@ class Poll extends Component {
|
||||
this.handleBackClick = this.handleBackClick.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { currentUser } = this.props;
|
||||
|
||||
if (!currentUser.presenter) {
|
||||
Session.set('openPanel', 'userlist');
|
||||
Session.set('forcePollOpen', false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleInputChange(index, event) {
|
||||
// This regex will replace any instance of 2 or more consecutive white spaces
|
||||
// with a single white space character.
|
||||
@ -100,21 +110,15 @@ class Poll extends Component {
|
||||
this.setState({ customPollValues: this.inputEditor });
|
||||
}
|
||||
|
||||
renderInputFields() {
|
||||
const { intl } = this.props;
|
||||
const items = [];
|
||||
handleBackClick() {
|
||||
const { stopPoll } = this.props;
|
||||
|
||||
items = _.range(1, MAX_CUSTOM_FIELDS + 1).map((ele, index) => (
|
||||
<input
|
||||
key={`custom-poll-${index}`}
|
||||
placeholder={intl.formatMessage(intlMessages.customPlaceholder)}
|
||||
className={styles.input}
|
||||
onChange={event => this.handleInputChange(index, event)}
|
||||
defaultValue={this.state.customPollValues[index]}
|
||||
/>
|
||||
));
|
||||
|
||||
return items;
|
||||
stopPoll();
|
||||
this.inputEditor = [];
|
||||
this.setState({
|
||||
isPolling: false,
|
||||
customPollValues: this.inputEditor,
|
||||
}, document.activeElement.blur());
|
||||
}
|
||||
|
||||
toggleCustomFields() {
|
||||
@ -133,7 +137,8 @@ class Poll extends Component {
|
||||
|
||||
const label = intl.formatMessage(
|
||||
// regex removes the - to match the message id
|
||||
intlMessages[type.replace(/-/g, '').toLowerCase()]);
|
||||
intlMessages[type.replace(/-/g, '').toLowerCase()],
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
@ -142,8 +147,8 @@ class Poll extends Component {
|
||||
className={styles.pollBtn}
|
||||
key={_.uniqueId('quick-poll-')}
|
||||
onClick={() => {
|
||||
this.setState({ isPolling: true }, () => startPoll(type));
|
||||
}}
|
||||
this.setState({ isPolling: true }, () => startPoll(type));
|
||||
}}
|
||||
/>);
|
||||
});
|
||||
|
||||
@ -172,15 +177,25 @@ class Poll extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
handleBackClick() {
|
||||
const { stopPoll } = this.props;
|
||||
renderInputFields() {
|
||||
const { intl } = this.props;
|
||||
const { customPollValues } = this.state;
|
||||
let items = [];
|
||||
|
||||
stopPoll();
|
||||
this.inputEditor = [];
|
||||
this.setState({
|
||||
isPolling: false,
|
||||
customPollValues: this.inputEditor,
|
||||
}, document.activeElement.blur());
|
||||
items = _.range(1, MAX_CUSTOM_FIELDS + 1).map((ele, index) => {
|
||||
const id = index;
|
||||
return (
|
||||
<input
|
||||
key={`custom-poll-${id}`}
|
||||
placeholder={intl.formatMessage(intlMessages.customPlaceholder)}
|
||||
className={styles.input}
|
||||
onChange={event => this.handleInputChange(id, event)}
|
||||
defaultValue={customPollValues[id]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
renderActivePollOptions() {
|
||||
@ -235,9 +250,13 @@ class Poll extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl, stopPoll, currentPoll,
|
||||
intl, stopPoll, currentPoll, currentUser,
|
||||
} = this.props;
|
||||
|
||||
const { isPolling } = this.state;
|
||||
|
||||
if (!currentUser.presenter) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header className={styles.header}>
|
||||
@ -248,22 +267,19 @@ class Poll extends Component {
|
||||
aria-label={intl.formatMessage(intlMessages.hidePollDesc)}
|
||||
className={styles.hideBtn}
|
||||
onClick={() => {
|
||||
Session.set('isPollOpen', false);
|
||||
Session.set('forcePollOpen', true);
|
||||
Session.set('isUserListOpen', true);
|
||||
Session.set('openPanel', 'userlist');
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
label={intl.formatMessage(intlMessages.closeLabel)}
|
||||
onClick={() => {
|
||||
if (currentPoll) {
|
||||
stopPoll();
|
||||
}
|
||||
Session.set('isPollOpen', false);
|
||||
Session.set('forcePollOpen', false);
|
||||
Session.set('isUserListOpen', true);
|
||||
}}
|
||||
if (currentPoll) {
|
||||
stopPoll();
|
||||
}
|
||||
Session.set('openPanel', 'userlist');
|
||||
Session.set('forcePollOpen', false);
|
||||
}}
|
||||
className={styles.closeBtn}
|
||||
icon="close"
|
||||
size="sm"
|
||||
@ -272,8 +288,8 @@ class Poll extends Component {
|
||||
|
||||
</header>
|
||||
{
|
||||
this.state.isPolling || !this.state.isPolling && currentPoll
|
||||
? this.renderActivePollOptions() : this.renderPollOptions()
|
||||
(isPolling || (!isPolling && currentPoll))
|
||||
? this.renderActivePollOptions() : this.renderPollOptions()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,23 +1,13 @@
|
||||
import React from 'react';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Users from '/imports/api/users';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Presentations from '/imports/api/presentations';
|
||||
import PresentationAreaService from '/imports/ui/components/presentation/service';
|
||||
import Poll from './component';
|
||||
import Service from './service';
|
||||
|
||||
const PollContainer = ({ ...props }) => {
|
||||
const currentUser = Users.findOne({ userId: Auth.userID });
|
||||
if (currentUser.presenter) {
|
||||
return (<Poll {...props} />);
|
||||
}
|
||||
Session.set('isPollOpen', false);
|
||||
Session.set('forcePollOpen', false);
|
||||
Session.set('isUserListOpen', true);
|
||||
return null;
|
||||
};
|
||||
const PollContainer = ({ ...props }) => <Poll {...props} />;
|
||||
|
||||
export default withTracker(({ }) => {
|
||||
Meteor.subscribe('current-poll', Auth.meetingID);
|
||||
|
@ -87,17 +87,19 @@ class LiveResult extends Component {
|
||||
answers.map((obj) => {
|
||||
const pct = Math.round(obj.numVotes / numRespondents * 100);
|
||||
|
||||
return pollStats.push(<div className={styles.main} key={_.uniqueId('stats-')}>
|
||||
<div className={styles.left}>
|
||||
{obj.key}
|
||||
</div>
|
||||
<div className={styles.center}>
|
||||
{obj.numVotes}
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
{`${isNaN(pct) ? 0 : pct}%`}
|
||||
</div>
|
||||
</div>);
|
||||
return pollStats.push(
|
||||
<div className={styles.main} key={_.uniqueId('stats-')}>
|
||||
<div className={styles.left}>
|
||||
{obj.key}
|
||||
</div>
|
||||
<div className={styles.center}>
|
||||
{obj.numVotes}
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
{`${Number.isNaN(pct) ? 0 : pct}%`}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
return pollStats;
|
||||
@ -117,8 +119,7 @@ class LiveResult extends Component {
|
||||
onClick={() => {
|
||||
publishPoll();
|
||||
stopPoll();
|
||||
Session.set('isUserListOpen', true);
|
||||
Session.set('isPollOpen', false);
|
||||
Session.set('openPanel', 'userlist');
|
||||
Session.set('forcePollOpen', false);
|
||||
}}
|
||||
label={intl.formatMessage(intlMessages.publishLabel)}
|
||||
@ -134,8 +135,12 @@ class LiveResult extends Component {
|
||||
className={styles.btn}
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.usersHeading}>{intl.formatMessage(intlMessages.usersTitle)}</div>
|
||||
<div className={styles.responseHeading}>{intl.formatMessage(intlMessages.responsesTitle)}</div>
|
||||
<div className={styles.usersHeading}>
|
||||
{intl.formatMessage(intlMessages.usersTitle)}
|
||||
</div>
|
||||
<div className={styles.responseHeading}>
|
||||
{intl.formatMessage(intlMessages.responsesTitle)}
|
||||
</div>
|
||||
{this.renderAnswers()}
|
||||
</div>
|
||||
</div>
|
||||
@ -149,4 +154,9 @@ LiveResult.propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
getUser: PropTypes.func.isRequired,
|
||||
currentPoll: PropTypes.arrayOf(Object).isRequired,
|
||||
publishPoll: PropTypes.func.isRequired,
|
||||
stopPoll: PropTypes.func.isRequired,
|
||||
handleBackClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -45,9 +45,13 @@ const defaultProps = {
|
||||
shortcuts: '',
|
||||
};
|
||||
|
||||
const toggleChatOpen = () => {
|
||||
Session.set('isChatOpen', !Session.get('isChatOpen'));
|
||||
Session.set('breakoutRoomIsOpen', false);
|
||||
const handleClickToggleChat = (id) => {
|
||||
Session.set(
|
||||
'openPanel',
|
||||
Session.get('openPanel') === 'chat' && Session.get('idChatOpen') === id
|
||||
? 'userlist' : 'chat',
|
||||
);
|
||||
Session.set('idChatOpen', id);
|
||||
};
|
||||
|
||||
const ChatListItem = (props) => {
|
||||
@ -73,15 +77,7 @@ const ChatListItem = (props) => {
|
||||
aria-expanded={isCurrentChat}
|
||||
tabIndex={tabIndex}
|
||||
accessKey={isPublicChat(chat) ? TOGGLE_CHAT_PUB_AK : null}
|
||||
onClick={() => {
|
||||
toggleChatOpen();
|
||||
Session.set('idChatOpen', chat.id);
|
||||
|
||||
if (Session.equals('isPollOpen', true)) {
|
||||
Session.set('isPollOpen', false);
|
||||
Session.set('forcePollOpen', true);
|
||||
}
|
||||
}}
|
||||
onClick={() => handleClickToggleChat(chat.id)}
|
||||
id="chat-toggle-button"
|
||||
aria-label={isPublicChat(chat) ? intl.formatMessage(intlMessages.titlePublic) : chat.name}
|
||||
>
|
||||
|
@ -41,11 +41,11 @@ const getCustomLogoUrl = () => Storage.getItem(CUSTOM_LOGO_URL_KEY);
|
||||
const sortUsersByName = (a, b) => {
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) {
|
||||
return -1;
|
||||
} else if (a.name.toLowerCase() > b.name.toLowerCase()) {
|
||||
} if (a.name.toLowerCase() > b.name.toLowerCase()) {
|
||||
return 1;
|
||||
} else if (a.id.toLowerCase() > b.id.toLowerCase()) {
|
||||
} if (a.id.toLowerCase() > b.id.toLowerCase()) {
|
||||
return -1;
|
||||
} else if (a.id.toLowerCase() < b.id.toLowerCase()) {
|
||||
} if (a.id.toLowerCase() < b.id.toLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -62,12 +62,12 @@ const sortUsersByEmoji = (a, b) => {
|
||||
if (emojiA && emojiB && (emojiA !== 'none' && emojiB !== 'none')) {
|
||||
if (a.emoji.changedAt < b.emoji.changedAt) {
|
||||
return -1;
|
||||
} else if (a.emoji.changedAt > b.emoji.changedAt) {
|
||||
} if (a.emoji.changedAt > b.emoji.changedAt) {
|
||||
return 1;
|
||||
}
|
||||
} else if (emojiA && emojiA !== 'none') {
|
||||
} if (emojiA && emojiA !== 'none') {
|
||||
return -1;
|
||||
} else if (emojiB && emojiB !== 'none') {
|
||||
} if (emojiB && emojiB !== 'none') {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
@ -76,9 +76,9 @@ const sortUsersByEmoji = (a, b) => {
|
||||
const sortUsersByModerator = (a, b) => {
|
||||
if (a.isModerator && b.isModerator) {
|
||||
return sortUsersByEmoji(a, b);
|
||||
} else if (a.isModerator) {
|
||||
} if (a.isModerator) {
|
||||
return -1;
|
||||
} else if (b.isModerator) {
|
||||
} if (b.isModerator) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -88,9 +88,9 @@ const sortUsersByModerator = (a, b) => {
|
||||
const sortUsersByPhoneUser = (a, b) => {
|
||||
if (!a.isPhoneUser && !b.isPhoneUser) {
|
||||
return 0;
|
||||
} else if (!a.isPhoneUser) {
|
||||
} if (!a.isPhoneUser) {
|
||||
return -1;
|
||||
} else if (!b.isPhoneUser) {
|
||||
} if (!b.isPhoneUser) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ const sortUsersByPhoneUser = (a, b) => {
|
||||
const sortUsersByCurrent = (a, b) => {
|
||||
if (a.isCurrent) {
|
||||
return -1;
|
||||
} else if (b.isCurrent) {
|
||||
} if (b.isCurrent) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -133,11 +133,11 @@ const sortUsers = (a, b) => {
|
||||
const sortChatsByName = (a, b) => {
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) {
|
||||
return -1;
|
||||
} else if (a.name.toLowerCase() > b.name.toLowerCase()) {
|
||||
} if (a.name.toLowerCase() > b.name.toLowerCase()) {
|
||||
return 1;
|
||||
} else if (a.id.toLowerCase() > b.id.toLowerCase()) {
|
||||
} if (a.id.toLowerCase() > b.id.toLowerCase()) {
|
||||
return -1;
|
||||
} else if (a.id.toLowerCase() < b.id.toLowerCase()) {
|
||||
} if (a.id.toLowerCase() < b.id.toLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -147,9 +147,9 @@ const sortChatsByName = (a, b) => {
|
||||
const sortChatsByIcon = (a, b) => {
|
||||
if (a.icon && b.icon) {
|
||||
return sortChatsByName(a, b);
|
||||
} else if (a.icon) {
|
||||
} if (a.icon) {
|
||||
return -1;
|
||||
} else if (b.icon) {
|
||||
} if (b.icon) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -436,6 +436,11 @@ const getGroupChatPrivate = (sender, receiver) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isUserModerator = (userId) => {
|
||||
const u = Users.findOne({ userId });
|
||||
return u ? u.moderator : false;
|
||||
};
|
||||
|
||||
export default {
|
||||
setEmojiStatus,
|
||||
assignPresenter,
|
||||
@ -457,7 +462,7 @@ export default {
|
||||
getCustomLogoUrl,
|
||||
getGroupChatPrivate,
|
||||
hasBreakoutRoom,
|
||||
isUserModerator,
|
||||
getEmojiList: () => EMOJI_STATUSES,
|
||||
getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji,
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { Session } from 'meteor/session';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
@ -11,10 +12,12 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
const toggleBreakoutPanel = () => {
|
||||
const breakoutPanelState = Session.get('breakoutRoomIsOpen');
|
||||
Session.set('breakoutRoomIsOpen', !breakoutPanelState);
|
||||
Session.set('isChatOpen', false);
|
||||
Session.set('isPollOpen', false);
|
||||
Session.set(
|
||||
'openPanel',
|
||||
Session.get('openPanel') === 'breakoutroom'
|
||||
? 'userlist'
|
||||
: 'breakoutroom',
|
||||
);
|
||||
};
|
||||
|
||||
const BreakoutRoomItem = ({
|
||||
@ -24,13 +27,17 @@ const BreakoutRoomItem = ({
|
||||
if (hasBreakoutRoom) {
|
||||
return (
|
||||
<div role="button" onClick={toggleBreakoutPanel}>
|
||||
<h2 className={styles.smallTitle}> {intl.formatMessage(intlMessages.breakoutTitle).toUpperCase()}</h2>
|
||||
<h2 className={styles.smallTitle}>
|
||||
{intl.formatMessage(intlMessages.breakoutTitle).toUpperCase()}
|
||||
</h2>
|
||||
<div className={styles.BreakoutRoomsItem}>
|
||||
<div className={styles.BreakoutRoomsContents}>
|
||||
<div className={styles.BreakoutRoomsIcon} >
|
||||
<div className={styles.BreakoutRoomsIcon}>
|
||||
<Icon iconName="rooms" />
|
||||
</div>
|
||||
<span className={styles.BreakoutRoomsText}>{intl.formatMessage(intlMessages.breakoutTitle)}</span>
|
||||
<span className={styles.BreakoutRoomsText}>
|
||||
{intl.formatMessage(intlMessages.breakoutTitle)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -40,3 +47,10 @@ const BreakoutRoomItem = ({
|
||||
};
|
||||
|
||||
export default injectIntl(BreakoutRoomItem);
|
||||
|
||||
BreakoutRoomItem.propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
hasBreakoutRoom: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
@ -72,12 +72,13 @@ class UserParticipants extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.compact) {
|
||||
const { compact, roving, users } = this.props;
|
||||
if (!compact) {
|
||||
this.refScrollContainer.addEventListener(
|
||||
'keydown',
|
||||
event => this.props.roving(
|
||||
event => roving(
|
||||
event,
|
||||
this.props.users.length,
|
||||
users.length,
|
||||
this.changeState,
|
||||
),
|
||||
);
|
||||
@ -91,12 +92,13 @@ class UserParticipants extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.index === -1) {
|
||||
const { index } = this.state;
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.index !== prevState.index) {
|
||||
this.focusUserItem(this.state.index);
|
||||
if (index !== prevState.index) {
|
||||
this.focusUserItem(index);
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,44 +131,43 @@ class UserParticipants extends Component {
|
||||
|
||||
const { meetingId } = meeting;
|
||||
|
||||
return users.map(u =>
|
||||
(
|
||||
<CSSTransition
|
||||
classNames={listTransition}
|
||||
appear
|
||||
enter
|
||||
exit
|
||||
timeout={0}
|
||||
component="div"
|
||||
className={cx(styles.participantsList)}
|
||||
key={u}
|
||||
>
|
||||
<div ref={(node) => { this.userRefs[index += 1] = node; }}>
|
||||
<UserListItemContainer
|
||||
{...{
|
||||
currentUser,
|
||||
compact,
|
||||
isBreakoutRoom,
|
||||
meetingId,
|
||||
getAvailableActions,
|
||||
normalizeEmojiName,
|
||||
isMeetingLocked,
|
||||
handleEmojiChange,
|
||||
getEmojiList,
|
||||
getEmoji,
|
||||
setEmojiStatus,
|
||||
assignPresenter,
|
||||
removeUser,
|
||||
toggleVoice,
|
||||
changeRole,
|
||||
getGroupChatPrivate,
|
||||
}}
|
||||
userId={u}
|
||||
getScrollContainerRef={this.getScrollContainerRef}
|
||||
/>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
));
|
||||
return users.map(u => (
|
||||
<CSSTransition
|
||||
classNames={listTransition}
|
||||
appear
|
||||
enter
|
||||
exit
|
||||
timeout={0}
|
||||
component="div"
|
||||
className={cx(styles.participantsList)}
|
||||
key={u}
|
||||
>
|
||||
<div ref={(node) => { this.userRefs[index += 1] = node; }}>
|
||||
<UserListItemContainer
|
||||
{...{
|
||||
currentUser,
|
||||
compact,
|
||||
isBreakoutRoom,
|
||||
meetingId,
|
||||
getAvailableActions,
|
||||
normalizeEmojiName,
|
||||
isMeetingLocked,
|
||||
handleEmojiChange,
|
||||
getEmojiList,
|
||||
getEmoji,
|
||||
setEmojiStatus,
|
||||
assignPresenter,
|
||||
removeUser,
|
||||
toggleVoice,
|
||||
changeRole,
|
||||
getGroupChatPrivate,
|
||||
}}
|
||||
userId={u}
|
||||
getScrollContainerRef={this.getScrollContainerRef}
|
||||
/>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
));
|
||||
}
|
||||
|
||||
focusUserItem(index) {
|
||||
@ -181,28 +182,40 @@ class UserParticipants extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl, users, compact, setEmojiStatus, muteAllUsers, meeting, muteAllExceptPresenter,
|
||||
intl,
|
||||
users,
|
||||
compact,
|
||||
setEmojiStatus,
|
||||
muteAllUsers,
|
||||
meeting,
|
||||
muteAllExceptPresenter,
|
||||
currentUser,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.userListColumn}>
|
||||
{
|
||||
!compact ?
|
||||
<div className={styles.container}>
|
||||
<h2 className={styles.smallTitle}>
|
||||
{intl.formatMessage(intlMessages.usersTitle)}
|
||||
({users.length})
|
||||
!compact
|
||||
? (
|
||||
<div className={styles.container}>
|
||||
<h2 className={styles.smallTitle}>
|
||||
{intl.formatMessage(intlMessages.usersTitle)}
|
||||
(
|
||||
{users.length}
|
||||
)
|
||||
|
||||
</h2>
|
||||
<UserOptionsContainer {...{
|
||||
users,
|
||||
muteAllUsers,
|
||||
muteAllExceptPresenter,
|
||||
setEmojiStatus,
|
||||
meeting,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</h2>
|
||||
<UserOptionsContainer {...{
|
||||
users,
|
||||
muteAllUsers,
|
||||
muteAllExceptPresenter,
|
||||
setEmojiStatus,
|
||||
meeting,
|
||||
currentUser,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: <hr className={styles.separator} />
|
||||
}
|
||||
<div
|
||||
|
@ -131,38 +131,52 @@ class UserDropdown extends PureComponent {
|
||||
this.seperator = _.uniqueId('action-separator-');
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (!this.state.isActionsOpen && this.state.showNestedOptions) {
|
||||
componentDidUpdate() {
|
||||
const { isActionsOpen, showNestedOptions } = this.state;
|
||||
|
||||
if (!isActionsOpen && showNestedOptions) {
|
||||
return this.resetMenuState();
|
||||
}
|
||||
|
||||
this.checkDropdownDirection();
|
||||
return this.checkDropdownDirection();
|
||||
}
|
||||
|
||||
makeDropdownItem(key, label, onClick, icon = null, iconRight = null) {
|
||||
return (
|
||||
<DropdownListItem
|
||||
{...{
|
||||
key,
|
||||
label,
|
||||
onClick,
|
||||
icon,
|
||||
iconRight,
|
||||
}}
|
||||
className={key === this.props.getEmoji ? styles.emojiSelected : null}
|
||||
data-test={key}
|
||||
/>
|
||||
);
|
||||
onActionsShow() {
|
||||
const { getScrollContainerRef } = this.props;
|
||||
const dropdown = this.getDropdownMenuParent();
|
||||
const scrollContainer = getScrollContainerRef();
|
||||
|
||||
if (dropdown && scrollContainer) {
|
||||
const dropdownTrigger = dropdown.children[0];
|
||||
const list = findDOMNode(this.list);
|
||||
const children = [].slice.call(list.children);
|
||||
children.find(child => child.getAttribute('role') === 'menuitem').focus();
|
||||
|
||||
this.setState({
|
||||
isActionsOpen: true,
|
||||
dropdownVisible: false,
|
||||
dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop,
|
||||
dropdownDirection: 'top',
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener('scroll', this.handleScroll, false);
|
||||
}
|
||||
}
|
||||
|
||||
resetMenuState() {
|
||||
return this.setState({
|
||||
onActionsHide(callback) {
|
||||
const { getScrollContainerRef } = this.props;
|
||||
|
||||
this.setState({
|
||||
isActionsOpen: false,
|
||||
dropdownOffset: 0,
|
||||
dropdownDirection: 'top',
|
||||
dropdownVisible: false,
|
||||
showNestedOptions: false,
|
||||
});
|
||||
|
||||
const scrollContainer = getScrollContainerRef();
|
||||
scrollContainer.removeEventListener('scroll', this.handleScroll, false);
|
||||
|
||||
if (callback) {
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
|
||||
getUsersActions() {
|
||||
@ -182,6 +196,8 @@ class UserDropdown extends PureComponent {
|
||||
changeRole,
|
||||
} = this.props;
|
||||
|
||||
const { showNestedOptions } = this.state;
|
||||
|
||||
const actionPermissions = getAvailableActions(currentUser, user, isBreakoutRoom);
|
||||
const actions = [];
|
||||
|
||||
@ -197,7 +213,7 @@ class UserDropdown extends PureComponent {
|
||||
allowedToChangeStatus,
|
||||
} = actionPermissions;
|
||||
|
||||
if (this.state.showNestedOptions) {
|
||||
if (showNestedOptions) {
|
||||
if (allowedToChangeStatus) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'back',
|
||||
@ -236,12 +252,8 @@ class UserDropdown extends PureComponent {
|
||||
intl.formatMessage(messages.ChatLabel),
|
||||
() => {
|
||||
getGroupChatPrivate(currentUser, user);
|
||||
if (Session.equals('isPollOpen', true)) {
|
||||
Session.set('isPollOpen', false);
|
||||
Session.set('forcePollOpen', true);
|
||||
}
|
||||
Session.set('openPanel', 'chat');
|
||||
Session.set('idChatOpen', user.id);
|
||||
Session.set('isChatOpen', true);
|
||||
},
|
||||
'chat',
|
||||
));
|
||||
@ -313,45 +325,38 @@ class UserDropdown extends PureComponent {
|
||||
return actions;
|
||||
}
|
||||
|
||||
onActionsShow() {
|
||||
const dropdown = this.getDropdownMenuParent();
|
||||
const scrollContainer = this.props.getScrollContainerRef();
|
||||
|
||||
if (dropdown && scrollContainer) {
|
||||
const dropdownTrigger = dropdown.children[0];
|
||||
const list = findDOMNode(this.list);
|
||||
const children = [].slice.call(list.children);
|
||||
children.find(child => child.getAttribute('role') === 'menuitem').focus();
|
||||
|
||||
this.setState({
|
||||
isActionsOpen: true,
|
||||
dropdownVisible: false,
|
||||
dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop,
|
||||
dropdownDirection: 'top',
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener('scroll', this.handleScroll, false);
|
||||
}
|
||||
}
|
||||
|
||||
onActionsHide(callback) {
|
||||
this.setState({
|
||||
isActionsOpen: false,
|
||||
dropdownVisible: false,
|
||||
});
|
||||
|
||||
const scrollContainer = this.props.getScrollContainerRef();
|
||||
scrollContainer.removeEventListener('scroll', this.handleScroll, false);
|
||||
|
||||
if (callback) {
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
|
||||
getDropdownMenuParent() {
|
||||
return findDOMNode(this.dropdown);
|
||||
}
|
||||
|
||||
makeDropdownItem(key, label, onClick, icon = null, iconRight = null) {
|
||||
const { getEmoji } = this.props;
|
||||
return (
|
||||
<DropdownListItem
|
||||
{...{
|
||||
key,
|
||||
label,
|
||||
onClick,
|
||||
icon,
|
||||
iconRight,
|
||||
}}
|
||||
className={key === getEmoji ? styles.emojiSelected : null}
|
||||
data-test={key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
resetMenuState() {
|
||||
return this.setState({
|
||||
isActionsOpen: false,
|
||||
dropdownOffset: 0,
|
||||
dropdownDirection: 'top',
|
||||
dropdownVisible: false,
|
||||
showNestedOptions: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleScroll() {
|
||||
this.setState({ isActionsOpen: false });
|
||||
}
|
||||
@ -360,12 +365,13 @@ class UserDropdown extends PureComponent {
|
||||
* Check if the dropdown is visible, if so, check if should be draw on top or bottom direction.
|
||||
*/
|
||||
checkDropdownDirection() {
|
||||
const { getScrollContainerRef } = this.props;
|
||||
if (this.isDropdownActivedByUser()) {
|
||||
const dropdown = this.getDropdownMenuParent();
|
||||
const dropdownTrigger = dropdown.children[0];
|
||||
const dropdownContent = dropdown.children[1];
|
||||
|
||||
const scrollContainer = this.props.getScrollContainerRef();
|
||||
const scrollContainer = getScrollContainerRef();
|
||||
|
||||
const nextState = {
|
||||
dropdownVisible: true,
|
||||
@ -377,7 +383,7 @@ class UserDropdown extends PureComponent {
|
||||
);
|
||||
|
||||
if (!isDropdownVisible) {
|
||||
const offsetPageTop = ((dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight) - scrollContainer.scrollTop);
|
||||
const offsetPageTop = (dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight) - scrollContainer.scrollTop;
|
||||
|
||||
nextState.dropdownOffset = window.innerHeight - offsetPageTop;
|
||||
nextState.dropdownDirection = 'bottom';
|
||||
@ -449,8 +455,8 @@ class UserDropdown extends PureComponent {
|
||||
const userItemContentsStyle = {};
|
||||
|
||||
userItemContentsStyle[styles.dropdown] = true;
|
||||
userItemContentsStyle[styles.userListItem] = !this.state.isActionsOpen;
|
||||
userItemContentsStyle[styles.usertListItemWithMenu] = this.state.isActionsOpen;
|
||||
userItemContentsStyle[styles.userListItem] = !isActionsOpen;
|
||||
userItemContentsStyle[styles.usertListItemWithMenu] = isActionsOpen;
|
||||
|
||||
const you = (user.isCurrent) ? intl.formatMessage(messages.you) : '';
|
||||
|
||||
@ -503,7 +509,7 @@ class UserDropdown extends PureComponent {
|
||||
return (
|
||||
<Dropdown
|
||||
ref={(ref) => { this.dropdown = ref; }}
|
||||
isOpen={this.state.isActionsOpen}
|
||||
isOpen={isActionsOpen}
|
||||
onShow={this.onActionsShow}
|
||||
onHide={this.onActionsHide}
|
||||
className={userItemContentsStyle}
|
||||
|
@ -20,6 +20,7 @@ const propTypes = {
|
||||
toggleMuteAllUsers: PropTypes.func.isRequired,
|
||||
toggleMuteAllUsersExceptPresenter: PropTypes.func.isRequired,
|
||||
toggleStatus: PropTypes.func.isRequired,
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -83,7 +84,14 @@ class UserOptions extends PureComponent {
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { intl, isMeetingMuted, mountModal } = this.props;
|
||||
const {
|
||||
intl,
|
||||
isMeetingMuted,
|
||||
mountModal,
|
||||
toggleStatus,
|
||||
toggleMuteAllUsers,
|
||||
toggleMuteAllUsersExceptPresenter,
|
||||
} = this.props;
|
||||
|
||||
this.menuItems = _.compact([
|
||||
(<DropdownListItem
|
||||
@ -91,21 +99,21 @@ class UserOptions extends PureComponent {
|
||||
icon="clear_status"
|
||||
label={intl.formatMessage(intlMessages.clearAllLabel)}
|
||||
description={intl.formatMessage(intlMessages.clearAllDesc)}
|
||||
onClick={this.props.toggleStatus}
|
||||
onClick={toggleStatus}
|
||||
/>),
|
||||
(<DropdownListItem
|
||||
key={_.uniqueId('list-item-')}
|
||||
icon="mute"
|
||||
label={intl.formatMessage(intlMessages.muteAllLabel)}
|
||||
description={intl.formatMessage(intlMessages.muteAllDesc)}
|
||||
onClick={this.props.toggleMuteAllUsers}
|
||||
onClick={toggleMuteAllUsers}
|
||||
/>),
|
||||
(<DropdownListItem
|
||||
key={_.uniqueId('list-item-')}
|
||||
icon="mute"
|
||||
label={intl.formatMessage(intlMessages.muteAllExceptPresenterLabel)}
|
||||
description={intl.formatMessage(intlMessages.muteAllExceptPresenterDesc)}
|
||||
onClick={this.props.toggleMuteAllUsersExceptPresenter}
|
||||
onClick={toggleMuteAllUsersExceptPresenter}
|
||||
/>),
|
||||
(<DropdownListItem
|
||||
key={_.uniqueId('list-item-')}
|
||||
@ -122,7 +130,8 @@ class UserOptions extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.isMeetingMuted !== this.props.isMeetingMuted) {
|
||||
const { isMeetingMuted } = this.props;
|
||||
if (prevProps.isMeetingMuted !== isMeetingMuted) {
|
||||
this.alterMenu();
|
||||
}
|
||||
}
|
||||
@ -140,16 +149,23 @@ class UserOptions extends PureComponent {
|
||||
}
|
||||
|
||||
alterMenu() {
|
||||
const { intl, isMeetingMuted } = this.props;
|
||||
const {
|
||||
intl,
|
||||
isMeetingMuted,
|
||||
toggleMuteAllUsers,
|
||||
toggleMuteAllUsersExceptPresenter,
|
||||
} = this.props;
|
||||
|
||||
if (isMeetingMuted) {
|
||||
const menuButton = (<DropdownListItem
|
||||
key={_.uniqueId('list-item-')}
|
||||
icon="unmute"
|
||||
label={intl.formatMessage(intlMessages.unmuteAllLabel)}
|
||||
description={intl.formatMessage(intlMessages.unmuteAllDesc)}
|
||||
onClick={this.props.toggleMuteAllUsers}
|
||||
/>);
|
||||
const menuButton = (
|
||||
<DropdownListItem
|
||||
key={_.uniqueId('list-item-')}
|
||||
icon="unmute"
|
||||
label={intl.formatMessage(intlMessages.unmuteAllLabel)}
|
||||
description={intl.formatMessage(intlMessages.unmuteAllDesc)}
|
||||
onClick={toggleMuteAllUsers}
|
||||
/>
|
||||
);
|
||||
this.menuItems.splice(1, 2, menuButton);
|
||||
} else {
|
||||
const muteMeetingButtons = [(<DropdownListItem
|
||||
@ -157,13 +173,13 @@ class UserOptions extends PureComponent {
|
||||
icon="mute"
|
||||
label={intl.formatMessage(intlMessages.muteAllLabel)}
|
||||
description={intl.formatMessage(intlMessages.muteAllDesc)}
|
||||
onClick={this.props.toggleMuteAllUsers}
|
||||
onClick={toggleMuteAllUsers}
|
||||
/>), (<DropdownListItem
|
||||
key={_.uniqueId('list-item-')}
|
||||
icon="mute"
|
||||
label={intl.formatMessage(intlMessages.muteAllExceptPresenterLabel)}
|
||||
description={intl.formatMessage(intlMessages.muteAllExceptPresenterDesc)}
|
||||
onClick={this.props.toggleMuteAllUsersExceptPresenter}
|
||||
onClick={toggleMuteAllUsersExceptPresenter}
|
||||
/>)];
|
||||
|
||||
this.menuItems.splice(1, 1, muteMeetingButtons[0], muteMeetingButtons[1]);
|
||||
@ -172,12 +188,13 @@ class UserOptions extends PureComponent {
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
const { isUserOptionsOpen } = this.state;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={(ref) => { this.dropdown = ref; }}
|
||||
autoFocus={false}
|
||||
isOpen={this.state.isUserOptionsOpen}
|
||||
isOpen={isUserOptionsOpen}
|
||||
onShow={this.onActionsShow}
|
||||
onHide={this.onActionsHide}
|
||||
className={styles.dropdown}
|
||||
|
@ -1,8 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import mapUser from '/imports/ui/services/user/mapUser';
|
||||
import Users from '/imports/api/users/';
|
||||
import UserOptions from './component';
|
||||
|
||||
|
||||
@ -31,43 +29,37 @@ export default class UserOptionsContainer extends PureComponent {
|
||||
|
||||
muteMeeting() {
|
||||
const { muteAllUsers } = this.props;
|
||||
const currentUser = Users.findOne({ userId: Auth.userID });
|
||||
|
||||
muteAllUsers(currentUser.userId);
|
||||
muteAllUsers(Auth.userID);
|
||||
}
|
||||
|
||||
muteAllUsersExceptPresenter() {
|
||||
const { muteAllExceptPresenter } = this.props;
|
||||
const currentUser = Users.findOne({ userId: Auth.userID });
|
||||
|
||||
muteAllExceptPresenter(currentUser.userId);
|
||||
muteAllExceptPresenter(Auth.userID);
|
||||
}
|
||||
|
||||
handleClearStatus() {
|
||||
const { users, setEmojiStatus } = this.props;
|
||||
|
||||
users.forEach((user) => {
|
||||
if (user.emoji.status !== 'none') {
|
||||
setEmojiStatus(user.id, 'none');
|
||||
}
|
||||
users.forEach((id) => {
|
||||
setEmojiStatus(id, 'none');
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const currentUser = Users.findOne({ userId: Auth.userID });
|
||||
const currentUserIsModerator = mapUser(currentUser).isModerator;
|
||||
const { meeting } = this.props;
|
||||
const { currentUser } = this.props;
|
||||
const currentUserIsModerator = currentUser.isModerator;
|
||||
|
||||
this.state.meetingMuted = meeting.voiceProp.muteOnStart;
|
||||
const { meetingMuted } = this.state;
|
||||
|
||||
return (
|
||||
currentUserIsModerator ?
|
||||
<UserOptions
|
||||
toggleMuteAllUsers={this.muteMeeting}
|
||||
toggleMuteAllUsersExceptPresenter={this.muteAllUsersExceptPresenter}
|
||||
toggleStatus={this.handleClearStatus}
|
||||
isMeetingMuted={this.state.meetingMuted}
|
||||
/> : null
|
||||
currentUserIsModerator
|
||||
? (
|
||||
<UserOptions
|
||||
toggleMuteAllUsers={this.muteMeeting}
|
||||
toggleMuteAllUsersExceptPresenter={this.muteAllUsersExceptPresenter}
|
||||
toggleStatus={this.handleClearStatus}
|
||||
isMeetingMuted={meetingMuted}
|
||||
/>) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
import { Session } from 'meteor/session';
|
||||
@ -14,6 +14,15 @@ const intlMessages = defineMessages({
|
||||
|
||||
class UserPolls extends PureComponent {
|
||||
render() {
|
||||
const handleClickTogglePoll = () => {
|
||||
Session.set(
|
||||
'openPanel',
|
||||
Session.get('openPanel') === 'poll'
|
||||
? 'userlist'
|
||||
: 'poll',
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
intl,
|
||||
isPresenter,
|
||||
@ -36,17 +45,10 @@ class UserPolls extends PureComponent {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.pollLink}
|
||||
onClick={() => {
|
||||
Session.set('isChatOpen', false);
|
||||
Session.set('breakoutRoomIsOpen', false);
|
||||
|
||||
return Session.equals('isPollOpen', true)
|
||||
? Session.set('isPollOpen', false)
|
||||
: Session.set('isPollOpen', true);
|
||||
}}
|
||||
onClick={handleClickTogglePoll}
|
||||
>
|
||||
<Icon iconName="polling" className={styles.icon} />
|
||||
<span className={styles.label} >{intl.formatMessage(intlMessages.pollLabel)}</span>
|
||||
<span className={styles.label}>{intl.formatMessage(intlMessages.pollLabel)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -55,3 +57,12 @@ class UserPolls extends PureComponent {
|
||||
}
|
||||
|
||||
export default injectIntl(UserPolls);
|
||||
|
||||
UserPolls.propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
isPresenter: PropTypes.bool.isRequired,
|
||||
pollIsOpen: PropTypes.bool.isRequired,
|
||||
forcePollOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
@ -49,9 +49,25 @@ const intlMessages = defineMessages({
|
||||
id: 'app.videoPreview.webcamNotFoundLabel',
|
||||
description: 'Webcam not found label',
|
||||
},
|
||||
sharingError: {
|
||||
id: 'app.video.sharingError',
|
||||
description: 'Error on sharing webcam',
|
||||
permissionError: {
|
||||
id: 'app.video.permissionError',
|
||||
description: 'Error message for webcam permission',
|
||||
},
|
||||
NotFoundError: {
|
||||
id: 'app.video.notFoundError',
|
||||
description: 'error message when can not get webcam video',
|
||||
},
|
||||
NotAllowedError: {
|
||||
id: 'app.video.notAllowed',
|
||||
description: 'error message when webcam had permission denied',
|
||||
},
|
||||
NotSupportedError: {
|
||||
id: 'app.video.notSupportedError',
|
||||
description: 'error message when origin do not have ssl valid',
|
||||
},
|
||||
NotReadableError: {
|
||||
id: 'app.video.notReadableError',
|
||||
description: 'error message When the webcam is being used by other software',
|
||||
},
|
||||
});
|
||||
|
||||
@ -91,6 +107,16 @@ class VideoPreview extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handlegUMError(error) {
|
||||
const {
|
||||
intl,
|
||||
} = this.props;
|
||||
const errorMessage = intlMessages[error.name]
|
||||
|| intlMessages.permissionError;
|
||||
notify(intl.formatMessage(errorMessage), 'error', 'video');
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
handleSelectWebcam(event) {
|
||||
const {
|
||||
intl,
|
||||
@ -108,8 +134,7 @@ class VideoPreview extends Component {
|
||||
this.video.srcObject = stream;
|
||||
this.deviceStream = stream;
|
||||
}).catch((error) => {
|
||||
notify(intl.formatMessage(intlMessages.sharingError), 'error', 'video');
|
||||
logger.error(error);
|
||||
this.handlegUMError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -156,8 +181,9 @@ class VideoPreview extends Component {
|
||||
this.setState({ availableWebcams: webcams });
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
}).catch((error) => {
|
||||
this.setState({ isStartSharingDisabled: true });
|
||||
this.handlegUMError(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -17,10 +17,6 @@ const intlClientErrors = defineMessages({
|
||||
id: 'app.video.iceCandidateError',
|
||||
description: 'Error message for ice candidate fail',
|
||||
},
|
||||
sharingError: {
|
||||
id: 'app.video.sharingError',
|
||||
description: 'Error on sharing webcam',
|
||||
},
|
||||
chromeExtensionError: {
|
||||
id: 'app.video.chromeExtensionError',
|
||||
description: 'Error message for Chrome Extension not installed',
|
||||
@ -53,6 +49,10 @@ const intlClientErrors = defineMessages({
|
||||
id: 'app.video.iceConnectionStateError',
|
||||
description: 'Error message for ice connection state being failed',
|
||||
},
|
||||
mediaFlowTimeout: {
|
||||
id: 'app.video.mediaFlowTimeout1020',
|
||||
description: 'Error message when media could not go through the server within the specified period',
|
||||
},
|
||||
});
|
||||
|
||||
const intlSFUErrors = defineMessages({
|
||||
@ -62,7 +62,7 @@ const intlSFUErrors = defineMessages({
|
||||
},
|
||||
2001: {
|
||||
id: 'app.sfu.mediaServerOffline2001',
|
||||
description: 'error message when kurento is offline',
|
||||
description: 'error message when SFU is offline',
|
||||
},
|
||||
2002: {
|
||||
id: 'app.sfu.mediaServerNoResources2002',
|
||||
@ -80,6 +80,10 @@ const intlSFUErrors = defineMessages({
|
||||
id: 'app.sfu.serverIceStateFailed2022',
|
||||
description: 'Error message fired when the server endpoint transitioned to a FAILED ICE state',
|
||||
},
|
||||
2200: {
|
||||
id: 'app.sfu.mediaGenericError2200',
|
||||
description: 'Error message fired when the SFU component generated a generic error',
|
||||
},
|
||||
2202: {
|
||||
id: 'app.sfu.invalidSdp2202',
|
||||
description: 'Error message fired when the clients provides an invalid SDP',
|
||||
@ -116,6 +120,9 @@ class VideoProvider extends Component {
|
||||
this.videoTags = {};
|
||||
this.sharedWebcam = false;
|
||||
|
||||
this.createVideoTag = this.createVideoTag.bind(this);
|
||||
this.getStats = this.getStats.bind(this);
|
||||
this.stopGettingStats = this.stopGettingStats.bind(this);
|
||||
this.onWsOpen = this.onWsOpen.bind(this);
|
||||
this.onWsClose = this.onWsClose.bind(this);
|
||||
this.onWsMessage = this.onWsMessage.bind(this);
|
||||
@ -129,46 +136,10 @@ class VideoProvider extends Component {
|
||||
this.customGetStats = this.customGetStats.bind(this);
|
||||
}
|
||||
|
||||
logger(type, message, options = {}) {
|
||||
const { userId, userName } = this.props;
|
||||
const topic = options.topic || 'video';
|
||||
|
||||
logger[type]({ obj: Object.assign(options, { userId, userName, topic }) }, `[${topic}] ${message}`);
|
||||
}
|
||||
|
||||
_sendPauseStream(id, role, state) {
|
||||
this.sendMessage({
|
||||
cameraId: id,
|
||||
id: 'pause',
|
||||
type: 'video',
|
||||
role,
|
||||
state,
|
||||
});
|
||||
}
|
||||
|
||||
pauseViewers() {
|
||||
this.logger('debug', 'Calling pause in viewer streams');
|
||||
|
||||
Object.keys(this.webRtcPeers).forEach((id) => {
|
||||
if (this.props.userId !== id && this.webRtcPeers[id].started) {
|
||||
this._sendPauseStream(id, 'viewer', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unpauseViewers() {
|
||||
this.logger('debug', 'Calling un-pause in viewer streams');
|
||||
|
||||
Object.keys(this.webRtcPeers).forEach((id) => {
|
||||
if (id !== this.props.userId && this.webRtcPeers[id].started) {
|
||||
this._sendPauseStream(id, 'viewer', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.ws.addEventListener('open', this.onWsOpen);
|
||||
this.ws.addEventListener('close', this.onWsClose);
|
||||
this.ws.onopen = this.onWsOpen;
|
||||
this.ws.onclose = this.onWsClose;
|
||||
|
||||
window.addEventListener('online', this.openWs);
|
||||
window.addEventListener('offline', this.onWsClose);
|
||||
@ -177,7 +148,7 @@ class VideoProvider extends Component {
|
||||
componentDidMount() {
|
||||
document.addEventListener('joinVideo', this.shareWebcam); // TODO find a better way to do this
|
||||
document.addEventListener('exitVideo', this.unshareWebcam);
|
||||
this.ws.addEventListener('message', this.onWsMessage);
|
||||
this.ws.onmessage = this.onWsMessage;
|
||||
window.addEventListener('beforeunload', this.unshareWebcam);
|
||||
|
||||
this.visibility.onVisible(this.unpauseViewers);
|
||||
@ -199,9 +170,9 @@ class VideoProvider extends Component {
|
||||
document.removeEventListener('joinVideo', this.shareWebcam);
|
||||
document.removeEventListener('exitVideo', this.unshareWebcam);
|
||||
|
||||
this.ws.removeEventListener('message', this.onWsMessage);
|
||||
this.ws.removeEventListener('open', this.onWsOpen);
|
||||
this.ws.removeEventListener('close', this.onWsClose);
|
||||
this.ws.onmessage = null;
|
||||
this.ws.onopen = null;
|
||||
this.ws.onclose = null;
|
||||
|
||||
window.removeEventListener('online', this.openWs);
|
||||
window.removeEventListener('offline', this.onWsClose);
|
||||
@ -222,35 +193,6 @@ class VideoProvider extends Component {
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
onWsOpen() {
|
||||
this.logger('debug', '------ Websocket connection opened.', { topic: 'ws' });
|
||||
|
||||
// -- Resend queued messages that happened when socket was not connected
|
||||
while (this.wsQueue.length > 0) {
|
||||
this.sendMessage(this.wsQueue.pop());
|
||||
}
|
||||
|
||||
this.pingInterval = setInterval(this.ping.bind(this), PING_INTERVAL);
|
||||
|
||||
this.setState({ socketOpen: true });
|
||||
}
|
||||
|
||||
onWsClose(error) {
|
||||
this.logger('debug', '------ Websocket connection closed.', { topic: 'ws' });
|
||||
|
||||
this.stopWebRTCPeer(this.props.userId);
|
||||
clearInterval(this.pingInterval);
|
||||
|
||||
this.setState({ socketOpen: false });
|
||||
}
|
||||
|
||||
ping() {
|
||||
const message = {
|
||||
id: 'ping',
|
||||
};
|
||||
this.sendMessage(message);
|
||||
}
|
||||
|
||||
onWsMessage(msg) {
|
||||
const parsedMessage = JSON.parse(msg.data);
|
||||
|
||||
@ -274,7 +216,6 @@ class VideoProvider extends Component {
|
||||
break;
|
||||
|
||||
case 'pong':
|
||||
this.logger('debug', 'Received pong from server', { topic: 'ws' });
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
@ -284,6 +225,100 @@ class VideoProvider extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onWsClose() {
|
||||
const {
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
this.logger('debug', '------ Websocket connection closed.', { topic: 'ws' });
|
||||
|
||||
clearInterval(this.pingInterval);
|
||||
|
||||
if (this.sharedWebcam) {
|
||||
this.unshareWebcam();
|
||||
}
|
||||
// Notify user that the SFU component has gone offline
|
||||
this.notifyError(intl.formatMessage(intlSFUErrors[2001]));
|
||||
|
||||
this.setState({ socketOpen: false });
|
||||
}
|
||||
|
||||
onWsOpen() {
|
||||
this.logger('debug', '------ Websocket connection opened.', { topic: 'ws' });
|
||||
|
||||
// -- Resend queued messages that happened when socket was not connected
|
||||
while (this.wsQueue.length > 0) {
|
||||
this.sendMessage(this.wsQueue.pop());
|
||||
}
|
||||
|
||||
this.pingInterval = setInterval(this.ping.bind(this), PING_INTERVAL);
|
||||
|
||||
this.setState({ socketOpen: true });
|
||||
}
|
||||
|
||||
getStats(id, video, callback) {
|
||||
const peer = this.webRtcPeers[id];
|
||||
|
||||
const hasLocalStream = peer && peer.started === true
|
||||
&& peer.peerConnection.getLocalStreams().length > 0;
|
||||
const hasRemoteStream = peer && peer.started === true
|
||||
&& peer.peerConnection.getRemoteStreams().length > 0;
|
||||
|
||||
if (hasLocalStream) {
|
||||
this.monitorTrackStart(peer.peerConnection,
|
||||
peer.peerConnection.getLocalStreams()[0].getVideoTracks()[0], true, callback);
|
||||
} else if (hasRemoteStream) {
|
||||
this.monitorTrackStart(peer.peerConnection,
|
||||
peer.peerConnection.getRemoteStreams()[0].getVideoTracks()[0], false, callback);
|
||||
}
|
||||
}
|
||||
|
||||
logger(type, message, options = {}) {
|
||||
const { userId, userName } = this.props;
|
||||
const topic = options.topic || 'video';
|
||||
|
||||
logger[type]({ obj: Object.assign(options, { userId, userName, topic }) }, `[${topic}] ${message}`);
|
||||
}
|
||||
|
||||
_sendPauseStream(id, role, state) {
|
||||
this.sendMessage({
|
||||
cameraId: id,
|
||||
id: 'pause',
|
||||
type: 'video',
|
||||
role,
|
||||
state,
|
||||
});
|
||||
}
|
||||
|
||||
pauseViewers() {
|
||||
const { userId } = this.props;
|
||||
this.logger('debug', 'Calling pause in viewer streams');
|
||||
|
||||
Object.keys(this.webRtcPeers).forEach((id) => {
|
||||
if (userId !== id && this.webRtcPeers[id].started) {
|
||||
this._sendPauseStream(id, 'viewer', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unpauseViewers() {
|
||||
const { userId } = this.props;
|
||||
this.logger('debug', 'Calling un-pause in viewer streams');
|
||||
|
||||
Object.keys(this.webRtcPeers).forEach((id) => {
|
||||
if (id !== userId && this.webRtcPeers[id].started) {
|
||||
this._sendPauseStream(id, 'viewer', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ping() {
|
||||
const message = {
|
||||
id: 'ping',
|
||||
};
|
||||
this.sendMessage(message);
|
||||
}
|
||||
|
||||
sendMessage(message) {
|
||||
const { ws } = this;
|
||||
|
||||
@ -347,7 +382,7 @@ class VideoProvider extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
stopWebRTCPeer(id) {
|
||||
stopWebRTCPeer(id, restarting = false) {
|
||||
this.logger('info', 'Stopping webcam', { cameraId: id });
|
||||
const { userId } = this.props;
|
||||
const shareWebcam = id === userId;
|
||||
@ -369,9 +404,16 @@ class VideoProvider extends Component {
|
||||
cameraId: id,
|
||||
});
|
||||
|
||||
// Clear the shared camera fail timeout when destroying
|
||||
clearTimeout(this.restartTimeout[id]);
|
||||
delete this.restartTimeout[id];
|
||||
// Clear the shared camera media flow timeout when destroying it
|
||||
if (!restarting) {
|
||||
if (this.restartTimeout[id]) {
|
||||
clearTimeout(this.restartTimeout[id]);
|
||||
}
|
||||
|
||||
if (this.restartTimer[id]) {
|
||||
delete this.restartTimer[id];
|
||||
}
|
||||
}
|
||||
|
||||
this.destroyWebRTCPeer(id);
|
||||
}
|
||||
@ -393,6 +435,11 @@ class VideoProvider extends Component {
|
||||
const { meetingId, sessionToken, voiceBridge } = this.props;
|
||||
let iceServers = [];
|
||||
|
||||
// Check if there's connectivity to the SFU component
|
||||
if (!this.connectedToMediaServer()) {
|
||||
return this._webRTCOnError(2001, id, shareWebcam);
|
||||
}
|
||||
|
||||
// Check if the peer is already being processed
|
||||
if (this.webRtcPeers[id]) {
|
||||
return;
|
||||
@ -467,23 +514,25 @@ class VideoProvider extends Component {
|
||||
});
|
||||
});
|
||||
if (this.webRtcPeers[id].peerConnection) {
|
||||
this.webRtcPeers[id].peerConnection.oniceconnectionstatechange =
|
||||
this._getOnIceConnectionStateChangeCallback(id);
|
||||
this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = this._getOnIceConnectionStateChangeCallback(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getWebRTCStartTimeout(id, shareWebcam, peer) {
|
||||
const { intl } = this.props;
|
||||
_getWebRTCStartTimeout(id, shareWebcam) {
|
||||
const { intl, userId } = this.props;
|
||||
|
||||
return () => {
|
||||
this.logger('error', `Camera share has not suceeded in ${CAMERA_SHARE_FAILED_WAIT_TIME}`, { cameraId: id });
|
||||
|
||||
if (this.props.userId === id) {
|
||||
this.notifyError(intl.formatMessage(intlClientErrors.sharingError));
|
||||
this.stopWebRTCPeer(id);
|
||||
if (userId === id) {
|
||||
this.notifyError(intl.formatMessage(intlClientErrors.mediaFlowTimeout));
|
||||
this.stopWebRTCPeer(id, false);
|
||||
} else {
|
||||
this.stopWebRTCPeer(id);
|
||||
// Subscribers try to reconnect according to their timers if media could
|
||||
// not reach the server. That's why we pass the restarting flag as true
|
||||
// to the stop procedure as to not destroy the timers
|
||||
this.stopWebRTCPeer(id, true);
|
||||
this.createWebRTCPeer(id, shareWebcam);
|
||||
|
||||
// Increment reconnect interval
|
||||
@ -508,19 +557,13 @@ class VideoProvider extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_webRTCOnError(error, id, shareWebcam) {
|
||||
_webRTCOnError(error, id) {
|
||||
const { intl } = this.props;
|
||||
|
||||
this.logger('error', ' WebRTC peerObj create error', id);
|
||||
this.logger('error', error, id);
|
||||
const errorMessage = intlClientErrors[error.name]
|
||||
|| intlClientErrors.permissionError;
|
||||
|| intlSFUErrors[error] || intlClientErrors.permissionError;
|
||||
this.notifyError(intl.formatMessage(errorMessage));
|
||||
/* This notification error is displayed considering kurento-utils
|
||||
* returned the error 'The request is not allowed by the user agent
|
||||
* or the platform in the current context.', but there are other
|
||||
* errors that could be returned. */
|
||||
|
||||
this.stopWebRTCPeer(id);
|
||||
|
||||
return this.logger('error', errorMessage, { cameraId: id });
|
||||
@ -555,7 +598,7 @@ class VideoProvider extends Component {
|
||||
const { intl } = this.props;
|
||||
const peer = this.webRtcPeers[id];
|
||||
|
||||
return (event) => {
|
||||
return () => {
|
||||
const connectionState = peer.peerConnection.iceConnectionState;
|
||||
if (connectionState === 'failed' || connectionState === 'closed') {
|
||||
// prevent the same error from being detected multiple times
|
||||
@ -569,6 +612,7 @@ class VideoProvider extends Component {
|
||||
}
|
||||
|
||||
attachVideoStream(id) {
|
||||
const { userId } = this.props;
|
||||
const video = this.videoTags[id];
|
||||
if (video == null) {
|
||||
this.logger('warn', 'Peer', id, 'has not been started yet');
|
||||
@ -580,7 +624,7 @@ class VideoProvider extends Component {
|
||||
return; // Skip if the stream is already attached
|
||||
}
|
||||
|
||||
const isCurrent = id === this.props.userId;
|
||||
const isCurrent = id === userId;
|
||||
const peer = this.webRtcPeers[id];
|
||||
|
||||
const attachVideoStreamHelper = () => {
|
||||
@ -615,8 +659,9 @@ class VideoProvider extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
customGetStats(peer, mediaStreamTrack, callback, interval) {
|
||||
const statsState = this.state.stats;
|
||||
customGetStats(peer, mediaStreamTrack, callback) {
|
||||
const { stats } = this.state;
|
||||
const statsState = stats;
|
||||
let promise;
|
||||
try {
|
||||
promise = peer.getStats(mediaStreamTrack);
|
||||
@ -627,9 +672,9 @@ class VideoProvider extends Component {
|
||||
let videoInOrOutbound = {};
|
||||
results.forEach((res) => {
|
||||
if (res.type === 'ssrc' || res.type === 'inbound-rtp' || res.type === 'outbound-rtp') {
|
||||
res.packetsSent = parseInt(res.packetsSent);
|
||||
res.packetsLost = parseInt(res.packetsLost) || 0;
|
||||
res.packetsReceived = parseInt(res.packetsReceived);
|
||||
res.packetsSent = parseInt(res.packetsSent, 10);
|
||||
res.packetsLost = parseInt(res.packetsLost, 10) || 0;
|
||||
res.packetsReceived = parseInt(res.packetsReceived, 10);
|
||||
|
||||
if ((isNaN(res.packetsSent) && res.packetsReceived === 0)
|
||||
|| (res.type === 'outbound-rtp' && res.isRemote)) {
|
||||
@ -637,11 +682,11 @@ class VideoProvider extends Component {
|
||||
}
|
||||
|
||||
if (res.googFrameWidthReceived) {
|
||||
res.width = parseInt(res.googFrameWidthReceived);
|
||||
res.height = parseInt(res.googFrameHeightReceived);
|
||||
res.width = parseInt(res.googFrameWidthReceived, 10);
|
||||
res.height = parseInt(res.googFrameHeightReceived, 10);
|
||||
} else if (res.googFrameWidthSent) {
|
||||
res.width = parseInt(res.googFrameWidthSent);
|
||||
res.height = parseInt(res.googFrameHeightSent);
|
||||
res.width = parseInt(res.googFrameWidthSent, 10);
|
||||
res.height = parseInt(res.googFrameHeightSent, 10);
|
||||
}
|
||||
|
||||
// Extra fields available on Chrome
|
||||
@ -692,11 +737,14 @@ class VideoProvider extends Component {
|
||||
|
||||
const videoKbitsReceivedPerSecond = (videoIntervalBytesReceived * 8) / videoReceivedInterval;
|
||||
const videoKbitsSentPerSecond = (videoIntervalBytesSent * 8) / videoSentInterval;
|
||||
const videoPacketDuration = (videoIntervalPacketsSent / videoSentInterval) * 1000;
|
||||
|
||||
let videoLostPercentage,
|
||||
videoLostRecentPercentage,
|
||||
videoBitrate;
|
||||
let videoLostPercentage;
|
||||
|
||||
|
||||
let videoLostRecentPercentage;
|
||||
|
||||
|
||||
let videoBitrate;
|
||||
if (videoStats.packetsReceived > 0) { // Remote video
|
||||
videoLostPercentage = ((videoStats.packetsLost / ((videoStats.packetsLost + videoStats.packetsReceived) * 100)) || 0).toFixed(1);
|
||||
videoBitrate = Math.floor(videoKbitsReceivedPerSecond || 0);
|
||||
@ -775,24 +823,13 @@ class VideoProvider extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getStats(id, video, callback) {
|
||||
const peer = this.webRtcPeers[id];
|
||||
|
||||
const hasLocalStream = peer && peer.started === true && peer.peerConnection.getLocalStreams().length > 0;
|
||||
const hasRemoteStream = peer && peer.started === true && peer.peerConnection.getRemoteStreams().length > 0;
|
||||
|
||||
if (hasLocalStream) {
|
||||
this.monitorTrackStart(peer.peerConnection, peer.peerConnection.getLocalStreams()[0].getVideoTracks()[0], true, callback);
|
||||
} else if (hasRemoteStream) {
|
||||
this.monitorTrackStart(peer.peerConnection, peer.peerConnection.getRemoteStreams()[0].getVideoTracks()[0], false, callback);
|
||||
}
|
||||
}
|
||||
|
||||
stopGettingStats(id) {
|
||||
const peer = this.webRtcPeers[id];
|
||||
|
||||
const hasLocalStream = peer && peer.started === true && peer.peerConnection.getLocalStreams().length > 0;
|
||||
const hasRemoteStream = peer && peer.started === true && peer.peerConnection.getRemoteStreams().length > 0;
|
||||
const hasLocalStream = peer && peer.started === true
|
||||
&& peer.peerConnection.getLocalStreams().length > 0;
|
||||
const hasRemoteStream = peer && peer.started === true
|
||||
&& peer.peerConnection.getRemoteStreams().length > 0;
|
||||
|
||||
if (hasLocalStream) {
|
||||
this.monitorTrackStop(peer.peerConnection.getLocalStreams()[0].getVideoTracks()[0].id);
|
||||
@ -811,9 +848,9 @@ class VideoProvider extends Component {
|
||||
handlePlayStart(message) {
|
||||
const id = message.cameraId;
|
||||
const peer = this.webRtcPeers[id];
|
||||
const videoTag = this.videoTags[id];
|
||||
|
||||
if (peer) {
|
||||
const { userId } = this.props;
|
||||
this.logger('info', 'Handle play start for camera', { cameraId: id });
|
||||
|
||||
// Clear camera shared timeout when camera succesfully starts
|
||||
@ -827,7 +864,7 @@ class VideoProvider extends Component {
|
||||
this.attachVideoStream(id);
|
||||
}
|
||||
|
||||
if (id === this.props.userId) {
|
||||
if (id === userId) {
|
||||
VideoService.sendUserShareWebcam(id);
|
||||
VideoService.joinedVideo();
|
||||
}
|
||||
@ -844,7 +881,7 @@ class VideoProvider extends Component {
|
||||
if (message.streamId === userId) {
|
||||
this.unshareWebcam();
|
||||
this.notifyError(intl.formatMessage(intlSFUErrors[code]
|
||||
|| intlClientErrors.sharingError));
|
||||
|| intlSFUErrors[2200]));
|
||||
} else {
|
||||
this.stopWebRTCPeer(message.cameraId);
|
||||
}
|
||||
@ -857,36 +894,34 @@ class VideoProvider extends Component {
|
||||
}
|
||||
|
||||
shareWebcam() {
|
||||
const { intl } = this.props;
|
||||
|
||||
if (this.connectedToMediaServer()) {
|
||||
this.logger('info', 'Sharing webcam');
|
||||
this.sharedWebcam = true;
|
||||
VideoService.joiningVideo();
|
||||
} else {
|
||||
this.logger('debug', 'Error on sharing webcam');
|
||||
this.notifyError(intl.formatMessage(intlClientErrors.sharingError));
|
||||
}
|
||||
}
|
||||
|
||||
unshareWebcam() {
|
||||
const { userId } = this.props;
|
||||
this.logger('info', 'Unsharing webcam');
|
||||
|
||||
VideoService.sendUserUnshareWebcam(this.props.userId);
|
||||
VideoService.sendUserUnshareWebcam(userId);
|
||||
VideoService.exitedVideo();
|
||||
this.sharedWebcam = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.socketOpen) return null;
|
||||
const { socketOpen } = this.state;
|
||||
if (!socketOpen) return null;
|
||||
|
||||
const { users, enableVideoStats } = this.props;
|
||||
return (
|
||||
<VideoList
|
||||
users={this.props.users}
|
||||
onMount={this.createVideoTag.bind(this)}
|
||||
getStats={this.getStats.bind(this)}
|
||||
stopGettingStats={this.stopGettingStats.bind(this)}
|
||||
enableVideoStats={this.props.enableVideoStats}
|
||||
users={users}
|
||||
onMount={this.createVideoTag}
|
||||
getStats={this.getStats}
|
||||
stopGettingStats={this.stopGettingStats}
|
||||
enableVideoStats={enableVideoStats}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -4,8 +4,10 @@ import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import VideoProvider from './component';
|
||||
import VideoService from './service';
|
||||
|
||||
const VideoProviderContainer = ({ children, ...props }) =>
|
||||
(!props.users.length ? null : <VideoProvider {...props}>{children}</VideoProvider>);
|
||||
const VideoProviderContainer = ({ children, ...props }) => {
|
||||
const { users } = props;
|
||||
return (!users.length ? null : <VideoProvider {...props}>{children}</VideoProvider>);
|
||||
};
|
||||
|
||||
export default withTracker(() => ({
|
||||
meetingId: VideoService.meetingId(),
|
||||
|
@ -5,7 +5,6 @@ import Meetings from '/imports/api/meetings/';
|
||||
import Users from '/imports/api/users/';
|
||||
import mapUser from '/imports/ui/services/user/mapUser';
|
||||
import UserListService from '/imports/ui/components/user-list/service';
|
||||
import SessionStorage from '/imports/ui/services/storage/session';
|
||||
|
||||
class VideoService {
|
||||
constructor() {
|
||||
@ -100,8 +99,8 @@ class VideoService {
|
||||
}
|
||||
|
||||
webcamOnlyModerator() {
|
||||
const m = Meetings.findOne({ meetingId: Auth.meetingID });
|
||||
return m.usersProp.webcamsOnlyForModerator;
|
||||
const m = Meetings.findOne({ meetingId: Auth.meetingID }) || {};
|
||||
return m.usersProp ? m.usersProp.webcamsOnlyForModerator : false;
|
||||
}
|
||||
|
||||
isLocked() {
|
||||
@ -127,8 +126,8 @@ class VideoService {
|
||||
}
|
||||
|
||||
voiceBridge() {
|
||||
const voiceBridge = Meetings.findOne({ meetingId: Auth.meetingID }).voiceProp.voiceConf;
|
||||
return voiceBridge;
|
||||
const m = Meetings.findOne({ meetingId: Auth.meetingID }) || {};
|
||||
return m.voiceProp ? m.voiceProp.voiceConf : null;
|
||||
}
|
||||
|
||||
isConnected() {
|
||||
|
@ -387,6 +387,7 @@
|
||||
"app.video.notAllowed": "Missing permission for share webcam, please make sure your browser permissions",
|
||||
"app.video.notSupportedError": "Can share webcam video only with safe sources, make sure your SSL certificate is valid",
|
||||
"app.video.notReadableError": "Could not get webcam video. Please make sure another program is not using the webcam ",
|
||||
"app.video.mediaFlowTimeout1020": "Error 1020: media could not reach the server",
|
||||
"app.video.swapCam": "Swap",
|
||||
"app.video.swapCamDesc": "swap the direction of webcams",
|
||||
"app.video.videoMenu": "Video menu",
|
||||
@ -414,6 +415,7 @@
|
||||
"app.sfu.mediaServerRequestTimeout2003": "Error 2003: Media server requests are timing out",
|
||||
"app.sfu.serverIceGatheringFailed2021": "Error 2021: Media server cannot gather ICE candidates",
|
||||
"app.sfu.serverIceGatheringFailed2022": "Error 2022: Media server ICE connection failed",
|
||||
"app.sfu.mediaGenericError2200": "Error 2200: Media server failed to process request",
|
||||
"app.sfu.invalidSdp2202":"Error 2202: Client generated an invalid SDP",
|
||||
"app.sfu.noAvailableCodec2203": "Error 2203: Server could not find an appropriate codec",
|
||||
"app.meeting.endNotification.ok.label": "OK",
|
||||
|
1
labs/api/meetings-vx/.gitignore
vendored
1
labs/api/meetings-vx/.gitignore
vendored
@ -1 +0,0 @@
|
||||
node_modules
|
@ -1,65 +0,0 @@
|
||||
fs = require 'fs'
|
||||
{print} = require 'util'
|
||||
{spawn, exec} = require 'child_process'
|
||||
glob = require 'glob'
|
||||
|
||||
REPORTER = "min"
|
||||
|
||||
config = {}
|
||||
config.binPath = './node_modules/.bin/'
|
||||
|
||||
# cake test # run all tests
|
||||
# cake -f test/lib/file.coffee test # run the files passed
|
||||
# cake -b test # run all tests and stop at first failure
|
||||
option '-f', '--file [FILE*]', 'input file(s)'
|
||||
option '-b', '--bail', 'bail'
|
||||
task 'test', 'Run the test suite', (options) ->
|
||||
process.env.NODE_ENV = "test"
|
||||
testFiles = [
|
||||
|
||||
]
|
||||
testOpts = [
|
||||
'--require', 'coffee-script/register',
|
||||
'--compilers', 'coffee:coffee-script/register',
|
||||
'--require', 'should',
|
||||
'--colors',
|
||||
'--ignore-leaks',
|
||||
'--timeout', '15000',
|
||||
'--reporter', 'spec'
|
||||
]
|
||||
if options.bail? and options.bail
|
||||
testOpts = testOpts.concat('-b')
|
||||
|
||||
if options.file?
|
||||
if _.isArray(options.file)
|
||||
files = testFiles.concat(options.file)
|
||||
else
|
||||
files = testFiles.concat([options.file])
|
||||
for opt in testOpts.reverse()
|
||||
files.unshift opt
|
||||
run 'mocha', files
|
||||
|
||||
else
|
||||
glob 'test/**/*.coffee', (error, files) ->
|
||||
for opt in testOpts.reverse()
|
||||
files.unshift opt
|
||||
run 'mocha', files
|
||||
|
||||
# Internal methods
|
||||
|
||||
# Spawns an application with `options` and calls `onExit`
|
||||
# when it finishes.
|
||||
run = (bin, options, onExit) ->
|
||||
bin = config.binPath + bin
|
||||
console.log timeNow() + ' - running: ' + bin + ' ' + (if options? then options.join(' ') else "")
|
||||
cmd = spawn bin, options
|
||||
cmd.stdout.on 'data', (data) -> print data.toString()
|
||||
cmd.stderr.on 'data', (data) -> print data.toString()
|
||||
cmd.on 'exit', (code) ->
|
||||
console.log 'done.'
|
||||
onExit?(code, options)
|
||||
|
||||
# Returns a string with the current time to print out.
|
||||
timeNow = ->
|
||||
today = new Date()
|
||||
today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds()
|
@ -1,41 +0,0 @@
|
||||
exploringHapi
|
||||
=============
|
||||
|
||||
This was used as a playground for attempts to validate URL parameters
|
||||
and to calculate and compare checksum
|
||||
|
||||
Keywords: hapi, joi, OAuth, checksum, hmac_sha1
|
||||
|
||||
Instructions:
|
||||
=============
|
||||
from Terminal:
|
||||
$ coffee index.coffee
|
||||
Listening on http://x.x.x.x:PORT
|
||||
|
||||
go to the browser, open an MCONF API-MATE window
|
||||
modify the "server"(id="input-custom-server-url") field to http://x.x.x.x:PORT
|
||||
click on the link for creating a meeting ("create ...")
|
||||
|
||||
In the Terminal window you should see something like:
|
||||
the checksum from url is
|
||||
e8b540ab61a71c46ebc99e7250e2ca6372115d9a and mine is
|
||||
e8b540ab61a71c46ebc99e7250e2ca6372115d9a
|
||||
YAY! They match!
|
||||
|
||||
or
|
||||
|
||||
the checksum from url is
|
||||
e8b540ab61a71c46ebc99e7250e2ca6372115d9a and mine is
|
||||
dkfjhdkjfhlkafhdfklahfkfhfjhkgfeq349492a
|
||||
|
||||
The browser window will display
|
||||
"everything is fine" if the parameter validation was successful
|
||||
or Error if it was not
|
||||
|
||||
|
||||
LOGGING
|
||||
# To use for CLI
|
||||
npm install -g bunyan
|
||||
|
||||
https://github.com/trentm/node-bunyan
|
||||
|
@ -1,17 +0,0 @@
|
||||
Hapi = require("hapi")
|
||||
pack = require './package'
|
||||
routes = require './lib/routes'
|
||||
bunyan = require 'bunyan'
|
||||
|
||||
log = bunyan.createLogger({name: 'myapp'});
|
||||
log.info('hi')
|
||||
log.warn({lang: 'fr'}, 'au revoir')
|
||||
|
||||
server = Hapi.createServer("0.0.0.0", parseInt(process.env.PORT, 10) or 4000)
|
||||
|
||||
server.start(() ->
|
||||
log.info(['start'], pack.name + ' - web interface: ' + server.info.uri);
|
||||
)
|
||||
|
||||
server.route routes.routes
|
||||
|
@ -1,30 +0,0 @@
|
||||
hapi = require 'hapi'
|
||||
Joi = require 'joi'
|
||||
util = require './util'
|
||||
sha1 = require 'js-sha1'
|
||||
|
||||
sharedSecret = '8cd8ef52e8e101574e400365b55e11a6'
|
||||
|
||||
index = (req, resp) ->
|
||||
resp "Hello World!"
|
||||
|
||||
createHandler = (req, resp) ->
|
||||
console.log("CREATE: " + req.originalUrl )
|
||||
checksum = req.query.checksum
|
||||
console.log("checksum = [" + checksum + "]")
|
||||
|
||||
query = util.removeChecksumFromQuery(req.query)
|
||||
|
||||
baseString = util.buildCreateBaseString(query)
|
||||
ourChecksum = util.calculateChecksum("create", baseString, sharedSecret)
|
||||
|
||||
console.log "the checksum from url is \n" + checksum + " and mine is\n" + ourChecksum
|
||||
|
||||
if checksum isnt ourChecksum
|
||||
resp "Fail!"
|
||||
else
|
||||
resp "everything is fine"
|
||||
|
||||
|
||||
exports.index = index
|
||||
exports.create = createHandler
|
@ -1,33 +0,0 @@
|
||||
hapi = require 'hapi'
|
||||
handlers = require './handlers'
|
||||
Joi = require 'joi'
|
||||
|
||||
createValidation =
|
||||
attendeePW: Joi.string().max(20).required()
|
||||
checksum: Joi.string().required()
|
||||
meetingID: Joi.string().min(3).max(30).required()
|
||||
moderatorPW: Joi.string().required()
|
||||
name: Joi.string().regex(/[a-zA-Z0-9]{3,30}/)
|
||||
record: Joi.boolean()
|
||||
voiceBridge: Joi.string()
|
||||
welcome: Joi.string()
|
||||
|
||||
routes = [{
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
config: {
|
||||
handler: handlers.index
|
||||
}
|
||||
}, {
|
||||
method: "GET",
|
||||
path: "/bigbluebutton/api/create",
|
||||
config: {
|
||||
handler: handlers.create,
|
||||
validate: {
|
||||
query: createValidation
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
|
||||
exports.routes = routes;
|
@ -1,32 +0,0 @@
|
||||
sha1 = require 'js-sha1'
|
||||
|
||||
|
||||
|
||||
removeChecksumFromQuery = (query) ->
|
||||
for own propName of query
|
||||
console.log(propName + "=" + query[propName])
|
||||
delete query['checksum']
|
||||
query
|
||||
|
||||
buildCreateBaseString = (query) ->
|
||||
baseString = ""
|
||||
for own propName of query
|
||||
propVal = query[propName]
|
||||
if (propName == "welcome")
|
||||
propVal = encodeURIComponent(query.welcome).replace(/%20/g, '+').replace(/[!'()]/g, escape).replace(/\*/g, "%2A")
|
||||
baseString += propName + "=" + propVal + "&"
|
||||
console.log(propName + "=" + query[propName])
|
||||
|
||||
console.log("baseString=[" + baseString.slice(0, -1) + "]")
|
||||
|
||||
baseString.slice(0, -1)
|
||||
|
||||
calculateChecksum = (method, baseString, sharedSecret) ->
|
||||
qStr = method + baseString + sharedSecret
|
||||
console.log("[" + qStr + "]")
|
||||
sha1(qStr)
|
||||
|
||||
|
||||
exports.removeChecksumFromQuery = removeChecksumFromQuery
|
||||
exports.buildCreateBaseString = buildCreateBaseString
|
||||
exports.calculateChecksum = calculateChecksum
|
@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "exploringHapi",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "coffee index.coffee"
|
||||
},
|
||||
"dependencies": {
|
||||
"hapi": "2.6.0",
|
||||
"joi": "2.7.0",
|
||||
"oauth-signature": "1.1.3",
|
||||
"coffee-script": "1.7.1",
|
||||
"js-sha1": "0.1.1",
|
||||
"bunyan": "0.22.2",
|
||||
"glob": "3.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"coffee-script": "1.7.1",
|
||||
"mocha": "1.18.2",
|
||||
"should": "3.3.1",
|
||||
"glob": "3.2.6",
|
||||
"chai": "1.9.x"
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
|
||||
|
||||
|
@ -1,31 +0,0 @@
|
||||
assert = require("assert")
|
||||
oauth = require("oauth-signature")
|
||||
|
||||
describe "Array", ->
|
||||
|
||||
describe '#indexOf()', ->
|
||||
|
||||
it 'should return -1 when the value is not present', ->
|
||||
assert.equal(-1, [1,2,3].indexOf(5))
|
||||
|
||||
it "should calc checksum", ->
|
||||
httpMethod = 'GET'
|
||||
url = 'http://photos.example.net/photos'
|
||||
parameters = {
|
||||
oauth_consumer_key : 'dpf43f3p2l4k3l03',
|
||||
oauth_token : 'nnch734d00sl2jdk',
|
||||
oauth_nonce : 'kllo9940pd9333jh',
|
||||
oauth_timestamp : '1191242096',
|
||||
oauth_signature_method : 'HMAC-SHA1',
|
||||
oauth_version : '1.0',
|
||||
file : 'vacation.jpg',
|
||||
size : 'original'
|
||||
}
|
||||
consumerSecret = 'kd94hf93k423kf44'
|
||||
tokenSecret = 'pfkkdhi9sl3r4s00'
|
||||
encodedSignature = oauth.generate(httpMethod, url, parameters, consumerSecret, tokenSecret);
|
||||
console.log(encodedSignature)
|
||||
assert.equal(encodedSignature, "tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D")
|
||||
|
||||
|
||||
|
1
labs/api/meetings/.gitignore
vendored
1
labs/api/meetings/.gitignore
vendored
@ -1 +0,0 @@
|
||||
node_modules
|
@ -1,65 +0,0 @@
|
||||
fs = require 'fs'
|
||||
{print} = require 'util'
|
||||
{spawn, exec} = require 'child_process'
|
||||
glob = require 'glob'
|
||||
|
||||
REPORTER = "min"
|
||||
|
||||
config = {}
|
||||
config.binPath = './node_modules/.bin/'
|
||||
|
||||
# cake test # run all tests
|
||||
# cake -f test/lib/file.coffee test # run the files passed
|
||||
# cake -b test # run all tests and stop at first failure
|
||||
option '-f', '--file [FILE*]', 'input file(s)'
|
||||
option '-b', '--bail', 'bail'
|
||||
task 'test', 'Run the test suite', (options) ->
|
||||
process.env.NODE_ENV = "test"
|
||||
testFiles = [
|
||||
|
||||
]
|
||||
testOpts = [
|
||||
'--require', 'coffee-script/register',
|
||||
'--compilers', 'coffee:coffee-script/register',
|
||||
'--require', 'should',
|
||||
'--colors',
|
||||
'--ignore-leaks',
|
||||
'--timeout', '15000',
|
||||
'--reporter', 'spec'
|
||||
]
|
||||
if options.bail? and options.bail
|
||||
testOpts = testOpts.concat('-b')
|
||||
|
||||
if options.file?
|
||||
if _.isArray(options.file)
|
||||
files = testFiles.concat(options.file)
|
||||
else
|
||||
files = testFiles.concat([options.file])
|
||||
for opt in testOpts.reverse()
|
||||
files.unshift opt
|
||||
run 'mocha', files
|
||||
|
||||
else
|
||||
glob 'test/**/*.coffee', (error, files) ->
|
||||
for opt in testOpts.reverse()
|
||||
files.unshift opt
|
||||
run 'mocha', files
|
||||
|
||||
# Internal methods
|
||||
|
||||
# Spawns an application with `options` and calls `onExit`
|
||||
# when it finishes.
|
||||
run = (bin, options, onExit) ->
|
||||
bin = config.binPath + bin
|
||||
console.log timeNow() + ' - running: ' + bin + ' ' + (if options? then options.join(' ') else "")
|
||||
cmd = spawn bin, options
|
||||
cmd.stdout.on 'data', (data) -> print data.toString()
|
||||
cmd.stderr.on 'data', (data) -> print data.toString()
|
||||
cmd.on 'exit', (code) ->
|
||||
console.log 'done.'
|
||||
onExit?(code, options)
|
||||
|
||||
# Returns a string with the current time to print out.
|
||||
timeNow = ->
|
||||
today = new Date()
|
||||
today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds()
|
@ -1,41 +0,0 @@
|
||||
exploringHapi
|
||||
=============
|
||||
|
||||
This was used as a playground for attempts to validate URL parameters
|
||||
and to calculate and compare checksum
|
||||
|
||||
Keywords: hapi, joi, OAuth, checksum, hmac_sha1
|
||||
|
||||
Instructions:
|
||||
=============
|
||||
from Terminal:
|
||||
$ coffee index.coffee
|
||||
Listening on http://x.x.x.x:PORT
|
||||
|
||||
go to the browser, open an MCONF API-MATE window
|
||||
modify the "server"(id="input-custom-server-url") field to http://x.x.x.x:PORT
|
||||
click on the link for creating a meeting ("create ...")
|
||||
|
||||
In the Terminal window you should see something like:
|
||||
the checksum from url is
|
||||
e8b540ab61a71c46ebc99e7250e2ca6372115d9a and mine is
|
||||
e8b540ab61a71c46ebc99e7250e2ca6372115d9a
|
||||
YAY! They match!
|
||||
|
||||
or
|
||||
|
||||
the checksum from url is
|
||||
e8b540ab61a71c46ebc99e7250e2ca6372115d9a and mine is
|
||||
dkfjhdkjfhlkafhdfklahfkfhfjhkgfeq349492a
|
||||
|
||||
The browser window will display
|
||||
"everything is fine" if the parameter validation was successful
|
||||
or Error if it was not
|
||||
|
||||
|
||||
LOGGING
|
||||
# To use for CLI
|
||||
npm install -g bunyan
|
||||
|
||||
https://github.com/trentm/node-bunyan
|
||||
|
@ -1,17 +0,0 @@
|
||||
Hapi = require("hapi")
|
||||
pack = require './package'
|
||||
routes = require './lib/routes'
|
||||
bunyan = require 'bunyan'
|
||||
|
||||
log = bunyan.createLogger({name: 'myapp'});
|
||||
log.info('hi')
|
||||
log.warn({lang: 'fr'}, 'au revoir')
|
||||
|
||||
server = Hapi.createServer("0.0.0.0", parseInt(process.env.PORT, 10) or 4000)
|
||||
|
||||
server.start(() ->
|
||||
log.info(['start'], pack.name + ' - web interface: ' + server.info.uri);
|
||||
)
|
||||
|
||||
server.route routes.routes
|
||||
|
@ -1,30 +0,0 @@
|
||||
hapi = require 'hapi'
|
||||
Joi = require 'joi'
|
||||
util = require './util'
|
||||
sha1 = require 'js-sha1'
|
||||
|
||||
sharedSecret = '8cd8ef52e8e101574e400365b55e11a6'
|
||||
|
||||
index = (req, resp) ->
|
||||
resp "Hello World!"
|
||||
|
||||
createHandler = (req, resp) ->
|
||||
console.log("CREATE: " + req.originalUrl )
|
||||
checksum = req.query.checksum
|
||||
console.log("checksum = [" + checksum + "]")
|
||||
|
||||
query = util.removeChecksumFromQuery(req.query)
|
||||
|
||||
baseString = util.buildCreateBaseString(query)
|
||||
ourChecksum = util.calculateChecksum("create", baseString, sharedSecret)
|
||||
|
||||
console.log "the checksum from url is \n" + checksum + " and mine is\n" + ourChecksum
|
||||
|
||||
if checksum isnt ourChecksum
|
||||
resp "Fail!"
|
||||
else
|
||||
resp "everything is fine"
|
||||
|
||||
|
||||
exports.index = index
|
||||
exports.create = createHandler
|
@ -1,33 +0,0 @@
|
||||
hapi = require 'hapi'
|
||||
handlers = require './handlers'
|
||||
Joi = require 'joi'
|
||||
|
||||
createValidation =
|
||||
attendeePW: Joi.string().max(20).required()
|
||||
checksum: Joi.string().required()
|
||||
meetingID: Joi.string().min(3).max(30).required()
|
||||
moderatorPW: Joi.string().required()
|
||||
name: Joi.string().regex(/[a-zA-Z0-9]{3,30}/)
|
||||
record: Joi.boolean()
|
||||
voiceBridge: Joi.string()
|
||||
welcome: Joi.string()
|
||||
|
||||
routes = [{
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
config: {
|
||||
handler: handlers.index
|
||||
}
|
||||
}, {
|
||||
method: "GET",
|
||||
path: "/bigbluebutton/api/create",
|
||||
config: {
|
||||
handler: handlers.create,
|
||||
validate: {
|
||||
query: createValidation
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
|
||||
exports.routes = routes;
|
@ -1,32 +0,0 @@
|
||||
sha1 = require 'js-sha1'
|
||||
|
||||
|
||||
|
||||
removeChecksumFromQuery = (query) ->
|
||||
for own propName of query
|
||||
console.log(propName + "=" + query[propName])
|
||||
delete query['checksum']
|
||||
query
|
||||
|
||||
buildCreateBaseString = (query) ->
|
||||
baseString = ""
|
||||
for own propName of query
|
||||
propVal = query[propName]
|
||||
if (propName == "welcome")
|
||||
propVal = encodeURIComponent(query.welcome).replace(/%20/g, '+').replace(/[!'()]/g, escape).replace(/\*/g, "%2A")
|
||||
baseString += propName + "=" + propVal + "&"
|
||||
console.log(propName + "=" + query[propName])
|
||||
|
||||
console.log("baseString=[" + baseString.slice(0, -1) + "]")
|
||||
|
||||
baseString.slice(0, -1)
|
||||
|
||||
calculateChecksum = (method, baseString, sharedSecret) ->
|
||||
qStr = method + baseString + sharedSecret
|
||||
console.log("[" + qStr + "]")
|
||||
sha1(qStr)
|
||||
|
||||
|
||||
exports.removeChecksumFromQuery = removeChecksumFromQuery
|
||||
exports.buildCreateBaseString = buildCreateBaseString
|
||||
exports.calculateChecksum = calculateChecksum
|
@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "meetingApi",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "coffee index.coffee"
|
||||
},
|
||||
"dependencies": {
|
||||
"hapi": "2.6.0",
|
||||
"joi": "2.7.0",
|
||||
"coffee-script": "1.7.1",
|
||||
"js-sha1": "0.1.1",
|
||||
"bunyan": "0.22.2",
|
||||
"glob": "3.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"coffee-script": "1.7.1",
|
||||
"mocha": "1.18.2",
|
||||
"should": "3.3.1",
|
||||
"glob": "3.2.6",
|
||||
"chai": "1.9.x"
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
hapi = require('hapi')
|
||||
assert = require('assert')
|
||||
chai = require('chai')
|
||||
assert = chai.assert
|
||||
routes = require('../lib/routes')
|
||||
|
||||
|
||||
# integration tests for API endpoint
|
||||
|
||||
|
||||
# setup server with firing up - use inject instead
|
||||
server = new hapi.Server()
|
||||
server.route(routes.routes)
|
||||
|
||||
|
||||
# parseurls endpoint test
|
||||
describe 'add endpoint', ->
|
||||
|
||||
it 'add - should add two numbers together', ->
|
||||
server.inject({method: 'PUT', url: '/sum/add/5/5'}, (res) ->
|
||||
assert.deepEqual({'equals': 10}, JSON.parse(res.payload))
|
||||
done()
|
||||
)
|
||||
|
||||
it 'add - should error if a string is passed', (done) ->
|
||||
server.inject({method: 'PUT', url: '/sum/add/100/x'}, (res) ->
|
||||
assert.deepEqual({
|
||||
'statusCode': 400,
|
||||
'error': 'Bad Request',
|
||||
'message': 'the value of b must be a number',
|
||||
'validation': {
|
||||
'source': 'path',
|
||||
'keys': [
|
||||
'b'
|
||||
]
|
||||
}
|
||||
}, JSON.parse(res.payload))
|
||||
done()
|
||||
)
|
@ -1,3 +0,0 @@
|
||||
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
assert = require("assert")
|
||||
|
||||
describe "Array", ->
|
||||
|
||||
describe '#indexOf()', ->
|
||||
|
||||
it 'should return -1 when the value is not present', ->
|
||||
assert.equal(-1, [1,2,3].indexOf(5))
|
||||
|
||||
|
||||
|
2
labs/api/recordings/.gitignore
vendored
2
labs/api/recordings/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
log/*.log
|
@ -1,65 +0,0 @@
|
||||
fs = require 'fs'
|
||||
{print} = require 'util'
|
||||
{spawn, exec} = require 'child_process'
|
||||
glob = require 'glob'
|
||||
|
||||
REPORTER = "min"
|
||||
|
||||
config = {}
|
||||
config.binPath = './node_modules/.bin/'
|
||||
|
||||
# cake test # run all tests
|
||||
# cake -f test/lib/file.coffee test # run the files passed
|
||||
# cake -b test # run all tests and stop at first failure
|
||||
option '-f', '--file [FILE*]', 'input file(s)'
|
||||
option '-b', '--bail', 'bail'
|
||||
task 'test', 'Run the test suite', (options) ->
|
||||
process.env.NODE_ENV = "test"
|
||||
testFiles = [
|
||||
|
||||
]
|
||||
testOpts = [
|
||||
'--require', 'coffee-script/register',
|
||||
'--compilers', 'coffee:coffee-script/register',
|
||||
'--require', 'should',
|
||||
'--colors',
|
||||
'--ignore-leaks',
|
||||
'--timeout', '15000',
|
||||
'--reporter', 'spec'
|
||||
]
|
||||
if options.bail? and options.bail
|
||||
testOpts = testOpts.concat('-b')
|
||||
|
||||
if options.file?
|
||||
if _.isArray(options.file)
|
||||
files = testFiles.concat(options.file)
|
||||
else
|
||||
files = testFiles.concat([options.file])
|
||||
for opt in testOpts.reverse()
|
||||
files.unshift opt
|
||||
run 'mocha', files
|
||||
|
||||
else
|
||||
glob 'test/**/*.coffee', (error, files) ->
|
||||
for opt in testOpts.reverse()
|
||||
files.unshift opt
|
||||
run 'mocha', files
|
||||
|
||||
# Internal methods
|
||||
|
||||
# Spawns an application with `options` and calls `onExit`
|
||||
# when it finishes.
|
||||
run = (bin, options, onExit) ->
|
||||
bin = config.binPath + bin
|
||||
console.log timeNow() + ' - running: ' + bin + ' ' + (if options? then options.join(' ') else "")
|
||||
cmd = spawn bin, options
|
||||
cmd.stdout.on 'data', (data) -> print data.toString()
|
||||
cmd.stderr.on 'data', (data) -> print data.toString()
|
||||
cmd.on 'exit', (code) ->
|
||||
console.log 'done.'
|
||||
onExit?(code, options)
|
||||
|
||||
# Returns a string with the current time to print out.
|
||||
timeNow = ->
|
||||
today = new Date()
|
||||
today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds()
|
@ -1,43 +0,0 @@
|
||||
recordingsWatcher
|
||||
=============
|
||||
This app is used to watch the file tree for recording files changes
|
||||
in the directories
|
||||
/var/bigbluebutton/published
|
||||
and
|
||||
/var/bigbluebutton/unpublished
|
||||
|
||||
|
||||
For each recording modified, we push into Redis:
|
||||
key = bbb:recordings:<meetingID>
|
||||
value = a set of JSON strings
|
||||
{"format": "<format>", "timestamp": "<timestamp>"}
|
||||
|
||||
|
||||
For example:
|
||||
|
||||
bbb:recordings:fbdbde6fd7b6499723a101c4c962f03843b4879c
|
||||
[{"format": "presentation", "timestamp": "1396623833035"}, {"format": "capture", "timestamp": "1396623833045"}]
|
||||
|
||||
|
||||
Instructions:
|
||||
=============
|
||||
from Terminal:
|
||||
$ coffee index.coffee
|
||||
|
||||
in another Terminal:
|
||||
$ curl localhost:4000/recordings?meetingid=fbdbde6fd7b6499723a101c4c962f03843b48
|
||||
returns an array of stringified json recordings (see above for the structure of the JSON)
|
||||
|
||||
if there are no recordings for the given meetingID, the message
|
||||
"No recordings for meetingid=some_random_string" appears
|
||||
|
||||
|
||||
Running Tests
|
||||
=============
|
||||
while the application is running // $ coffee index.coffee
|
||||
open another console and enter:
|
||||
$ cake test
|
||||
|
||||
or
|
||||
$ ./node_modules/.bin/mocha --require coffee-script/register --compilers coffee:coffee-script/register --require should --colors --ignore-leaks --timeout 15000 --reporter spec test/routetests.coffee
|
||||
(where test/routetests.coffee is the collecion of tests you want to execute)
|
@ -1,13 +0,0 @@
|
||||
# # Global configurations file
|
||||
|
||||
config = {}
|
||||
|
||||
# Logging
|
||||
config.log = {}
|
||||
|
||||
config.log.path = if process.env.NODE_ENV == "production"
|
||||
"/var/log/bigbluebutton/recording-api.log"
|
||||
else
|
||||
"./log/recording-api-dev.log"
|
||||
|
||||
module.exports = config
|
@ -1,17 +0,0 @@
|
||||
hapi = require 'hapi'
|
||||
|
||||
log = require './lib/logger'
|
||||
pack = require './package'
|
||||
recWatcher = require './lib/recording-dir-watcher'
|
||||
routes = require './lib/routes'
|
||||
|
||||
server = hapi.createServer("0.0.0.0",
|
||||
parseInt(process.env.PORT, 10) or 4000)
|
||||
|
||||
server.start(() ->
|
||||
log.info(['start'], pack.name + ' - web interface: ' + server.info.uri)
|
||||
)
|
||||
|
||||
server.route(routes.routes)
|
||||
|
||||
recWatcher.watch()
|
@ -1,38 +0,0 @@
|
||||
util = require './util'
|
||||
recWatcher = require './recording-dir-watcher'
|
||||
|
||||
sharedSecret = '8cd8ef52e8e101574e400365b55e11a6'
|
||||
|
||||
index = (req, resp) ->
|
||||
resp "Hello World!"
|
||||
|
||||
createHandler = (req, resp) ->
|
||||
console.log("CREATE: " + req.originalUrl )
|
||||
checksum = req.query.checksum
|
||||
console.log("checksum = [" + checksum + "]")
|
||||
|
||||
query = util.removeChecksumFromQuery(req.query)
|
||||
|
||||
baseString = util.buildCreateBaseString(query)
|
||||
ourChecksum = util.calculateChecksum("create", baseString, sharedSecret)
|
||||
|
||||
console.log "the checksum from url is \n" + checksum + " and mine is\n" + ourChecksum
|
||||
|
||||
if checksum isnt ourChecksum
|
||||
resp "Fail!"
|
||||
else
|
||||
resp "everything is fine"
|
||||
|
||||
getRecordings = (req, resp) ->
|
||||
requestedMeetingID = req.query.meetingid
|
||||
console.log("recordings for: " + requestedMeetingID)
|
||||
|
||||
recWatcher.getRecordingsArray requestedMeetingID, (array) ->
|
||||
if array?.length > 0
|
||||
resp JSON.stringify(array)
|
||||
else
|
||||
resp "No recordings for meetingid=#{requestedMeetingID}\n"
|
||||
|
||||
exports.index = index
|
||||
exports.create = createHandler
|
||||
exports.recordings = getRecordings
|
@ -1,19 +0,0 @@
|
||||
bunyan = require 'bunyan'
|
||||
|
||||
config = require '../config'
|
||||
|
||||
logger = bunyan.createLogger({
|
||||
name: 'bbbnode'
|
||||
streams: [
|
||||
{
|
||||
level: 'debug'
|
||||
stream: process.stdout
|
||||
},
|
||||
{
|
||||
level: 'info'
|
||||
path: config.log.path
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
module.exports = logger
|
@ -1,57 +0,0 @@
|
||||
##
|
||||
## Watches the recording dirs for new recordings
|
||||
##
|
||||
|
||||
chokidar = require 'chokidar'
|
||||
redis = require 'redis'
|
||||
|
||||
log = require './logger'
|
||||
|
||||
|
||||
client = redis.createClient()
|
||||
|
||||
baseKey = 'bbb:recordings:'
|
||||
|
||||
watch = ->
|
||||
#clear the keys first
|
||||
keys = client.keys(baseKey.concat('*'))
|
||||
client.del(keys)
|
||||
|
||||
#start watching
|
||||
chokidar.watch('/var/bigbluebutton/published', {ignored: /[\/\\]\./}).on 'all', (event, path) ->
|
||||
somethingChanged(event,path)
|
||||
chokidar.watch('/var/bigbluebutton/unpublished', {ignored: /[\/\\]\./}).on 'all', (event, path) ->
|
||||
somethingChanged(event,path)
|
||||
|
||||
|
||||
somethingChanged = (event, path) ->
|
||||
uri = path.split('/')
|
||||
|
||||
if uri[5]? #excludes the parent directories being added
|
||||
pathArray = path.substring(path.lastIndexOf('/')+1).split('-')
|
||||
meetingID = pathArray[0]
|
||||
timestamp = pathArray[1]
|
||||
|
||||
thisKey = baseKey.concat(meetingID)
|
||||
|
||||
json = {
|
||||
"format": uri[4]
|
||||
"timestamp": timestamp
|
||||
}
|
||||
|
||||
log.info(event, path)
|
||||
str = JSON.stringify(json)
|
||||
|
||||
client.sadd(thisKey, str)
|
||||
|
||||
getRecordingsArray = (meetingID, callback) ->
|
||||
thisKey = baseKey.concat(meetingID)
|
||||
|
||||
client.smembers thisKey, (err, members) ->
|
||||
if err
|
||||
console.log "Error: #{err}"
|
||||
else
|
||||
callback members
|
||||
|
||||
exports.watch = watch
|
||||
exports.getRecordingsArray = getRecordingsArray
|
@ -1,44 +0,0 @@
|
||||
Joi = require 'joi'
|
||||
|
||||
handlers = require './handlers'
|
||||
|
||||
createValidation =
|
||||
attendeePW: Joi.string().max(20).required()
|
||||
checksum: Joi.string().required()
|
||||
meetingID: Joi.string().min(3).max(30).required()
|
||||
moderatorPW: Joi.string().required()
|
||||
name: Joi.string().regex(/[a-zA-Z0-9]{3,30}/)
|
||||
record: Joi.boolean()
|
||||
voiceBridge: Joi.string()
|
||||
welcome: Joi.string()
|
||||
|
||||
recordingsValidation =
|
||||
meetingid: Joi.string().min(3).max(45).required()
|
||||
|
||||
routes = [{
|
||||
method: 'GET'
|
||||
path: '/'
|
||||
config: {
|
||||
handler: handlers.index
|
||||
}
|
||||
}, {
|
||||
method: "GET"
|
||||
path: "/bigbluebutton/api/create"
|
||||
config: {
|
||||
handler: handlers.create
|
||||
validate: {
|
||||
query: createValidation
|
||||
}
|
||||
}
|
||||
}, {
|
||||
method: "GET"
|
||||
path: "/recordings"
|
||||
config: {
|
||||
handler: handlers.recordings
|
||||
validate: {
|
||||
query: recordingsValidation
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
exports.routes = routes
|
@ -1,12 +0,0 @@
|
||||
parser1 = require 'xml2json'
|
||||
parser2 = require 'json2xml'
|
||||
|
||||
xml2json = (xmlStr) ->
|
||||
parser1.toJson(xmlStr)
|
||||
|
||||
json2xml = (jsonObj) ->
|
||||
#parser2(jsonObj)
|
||||
parser1.toXml(jsonObj)
|
||||
|
||||
exports.xml2json = xml2json
|
||||
exports.json2xml = json2xml
|
@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "recordingsWatcher",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "coffee index.coffee"
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "0.8.2",
|
||||
"redis": "0.10.1",
|
||||
"hiredis": "0.1.16",
|
||||
"hapi": "2.6.0",
|
||||
"joi": "2.7.0",
|
||||
"coffee-script": "1.7.1",
|
||||
"js-sha1": "0.1.1",
|
||||
"bunyan": "0.22.2",
|
||||
"json2xml": "0.1.1",
|
||||
"xml2json": "0.4.0",
|
||||
"easyxml": "0.0.5",
|
||||
"glob": "3.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"coffee-script": "1.7.1",
|
||||
"mocha": "1.18.2",
|
||||
"should": "3.3.1",
|
||||
"glob": "3.2.6",
|
||||
"chai": "1.9.x"
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
assert = require('chai').assert
|
||||
hapi = require('hapi')
|
||||
|
||||
routes = require('../lib/routes')
|
||||
|
||||
# integration tests for API endpoint
|
||||
|
||||
|
||||
# setup server with firing up - use inject instead
|
||||
server = new hapi.Server()
|
||||
server.route(routes.routes)
|
||||
|
||||
|
||||
# parseurls endpoint test
|
||||
describe 'checking recordings', ->
|
||||
|
||||
it 'recordings for a given meetingid', ->
|
||||
server.inject({method: 'GET', url: '192.168.0.203:4000/recordings?meetingid=fbdbde6fd7b6499723a101c4c962f03843b4879c'}, (res) ->
|
||||
#console.log "json:" + res.payload
|
||||
array = [
|
||||
{
|
||||
'format': 'presentation'
|
||||
'timestamp':'1396619572523'
|
||||
}, {
|
||||
'format': 'capture'
|
||||
'timestamp':'1396623833044'
|
||||
}, {
|
||||
'format': 'presentation'
|
||||
'timestamp':'1396620788271'
|
||||
}, {
|
||||
'format': 'presentation'
|
||||
'timestamp':'1396622260421'
|
||||
}, {
|
||||
'format': 'capture'
|
||||
'timestamp':'1396623833035'
|
||||
}, {
|
||||
'format': 'capture'
|
||||
'timestamp':'1396623830000'
|
||||
}, {
|
||||
'format': 'capture'
|
||||
'timestamp':'1396619572523'
|
||||
}, {
|
||||
'format': 'capture'
|
||||
'timestamp':'1396622260421'
|
||||
}, {
|
||||
'format': 'capture'
|
||||
'timestamp':'1396620788271'
|
||||
}, {
|
||||
'format': 'presentation'
|
||||
'timestamp':'1396623833035'
|
||||
}, {
|
||||
'format': 'capture'
|
||||
'timestamp':'1396623831111'
|
||||
}
|
||||
]
|
||||
|
||||
parsedOnce = JSON.parse(res.payload)
|
||||
index = 0
|
||||
while index < parsedOnce.length
|
||||
assert.deepEqual(JSON.stringify(array[index]), parsedOnce[index])
|
||||
index++
|
||||
#console.log index
|
||||
)
|
||||
###it 'add - should add two numbers together', ->
|
||||
server.inject({method: 'PUT', url: '/sum/add/5/5'}, (res) ->
|
||||
console.log "json:" +JSON.stringify(res.payload)
|
||||
assert.deepEqual({'equals': 10}, JSON.parse(res.payload))
|
||||
done()
|
||||
)###
|
||||
|
||||
###it 'add - should error if a string is passed', (done) ->
|
||||
server.inject({method: 'PUT', url: '/sum/add/100/1'}, (res) ->
|
||||
console.log "json:" +JSON.stringify(res)
|
||||
assert.deepEqual({
|
||||
'statusCode': 400
|
||||
'error': 'Bad Request'
|
||||
'message': 'the value of b must be a number'
|
||||
'validation': {
|
||||
'source': 'path'
|
||||
'keys': [
|
||||
'b'
|
||||
]
|
||||
}
|
||||
}, JSON.parse(res.payload))
|
||||
done()
|
||||
)###
|
@ -1,3 +0,0 @@
|
||||
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
assert = require("assert")
|
||||
|
||||
describe "Array", ->
|
||||
|
||||
describe '#indexOf()', ->
|
||||
|
||||
it 'should return -1 when the value is not present', ->
|
||||
assert.equal(-1, [1,2,3].indexOf(5))
|
||||
|
||||
|
||||
|
@ -1,58 +0,0 @@
|
||||
assert = require("assert")
|
||||
|
||||
util = require '../lib/util'
|
||||
|
||||
sampleXml = """
|
||||
<recording>
|
||||
<id>6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956</id>
|
||||
<state>available</state>
|
||||
<published>true</published>
|
||||
<start_time>1398363223514</start_time>
|
||||
<end_time>1398363348994</end_time>
|
||||
<playback>
|
||||
<format>presentation</format>
|
||||
<link>http://example.com/playback/presentation/playback.html?meetingID=6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956</link>
|
||||
<processing_time>5429</processing_time>
|
||||
<duration>101014</duration>
|
||||
<extension>
|
||||
<custom>... Any XML element, to be passed through into playback format element.</custom>
|
||||
</extension>
|
||||
</playback>
|
||||
<meta>
|
||||
<meetingId>English 101</meetingId>
|
||||
<meetingName>English 101</meetingName>
|
||||
<description>Test recording</description>
|
||||
<title>English 101</title>
|
||||
</meta>
|
||||
</recording>
|
||||
"""
|
||||
|
||||
jsonResult = {
|
||||
"recording": {
|
||||
"id": "6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956",
|
||||
"state": "available",
|
||||
"published": true,
|
||||
"start_time": 1398363223514,
|
||||
"end_time": 1398363348994,
|
||||
"playback": {
|
||||
"format": "presentation",
|
||||
"link": "http://example.com/playback/presentation/playback.html?meetingID=6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956",
|
||||
"processing_time": 5429,
|
||||
"duration": 101014,
|
||||
"extension": {
|
||||
"custom": "... Any XML element, to be passed through into playback format element."
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"meetingId": "English 101",
|
||||
"meetingName": "English 101",
|
||||
"description": "Test recording",
|
||||
"title": "English 101"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe "util", ->
|
||||
describe 'xml2json()', ->
|
||||
it 'should return a json string', ->
|
||||
assert.deepEqual(jsonResult, JSON.parse(util.xml2json(sampleXml)))
|
@ -1,34 +0,0 @@
|
||||
util = require './lib/util'
|
||||
|
||||
sampleXml = """
|
||||
<recording>
|
||||
<id>6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956</id>
|
||||
<state>available</state>
|
||||
<published>true</published>
|
||||
<start_time>1398363223514</start_time>
|
||||
<end_time>1398363348994</end_time>
|
||||
<playback>
|
||||
<format>presentation</format>
|
||||
<link>http://example.com/playback/presentation/playback.html?meetingID=6e35e3b2778883f5db637d7a5dba0a427f692e91-1398363221956</link>
|
||||
<processing_time>5429</processing_time>
|
||||
<duration>101014</duration>
|
||||
<extension>
|
||||
<custom>... Any XML element, to be passed through into playback format element.</custom>
|
||||
</extension>
|
||||
</playback>
|
||||
<meta>
|
||||
<meetingId>English 101</meetingId>
|
||||
<meetingName>English 101</meetingName>
|
||||
<description>Test recording</description>
|
||||
<title>English 101</title>
|
||||
</meta>
|
||||
</recording>
|
||||
"""
|
||||
|
||||
jsonObj = util.xml2json( sampleXml )
|
||||
|
||||
console.log(jsonObj)
|
||||
|
||||
jstr = util.json2xml(JSON.parse(jsonObj))
|
||||
|
||||
console.log(jstr)
|
@ -7,11 +7,11 @@ GEM
|
||||
java_properties (0.0.4)
|
||||
jwt (2.1.0)
|
||||
mini_portile2 (2.3.0)
|
||||
nokogiri (1.8.1)
|
||||
nokogiri (1.8.5)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
open4 (1.3.4)
|
||||
redis (4.0.1)
|
||||
rubyzip (1.2.1)
|
||||
rubyzip (1.2.2)
|
||||
trollop (2.1.2)
|
||||
|
||||
PLATFORMS
|
||||
|
Loading…
Reference in New Issue
Block a user