Merge remote-tracking branch 'upstream/develop' into connection-manager

This commit is contained in:
Joao Siebel 2020-10-13 10:49:12 -03:00
commit dadca62741
36 changed files with 308 additions and 130 deletions

View File

@ -56,7 +56,7 @@ trait RegisterUserReqMsgHdlr {
val g = GuestApprovedVO(regUser.id, GuestStatus.ALLOW)
UsersApp.approveOrRejectGuest(liveMeeting, outGW, g, SystemUser.ID)
case GuestStatus.WAIT =>
val guest = GuestWaiting(regUser.id, regUser.name, regUser.role, regUser.guest, regUser.authed)
val guest = GuestWaiting(regUser.id, regUser.name, regUser.role, regUser.guest, regUser.avatarURL, regUser.authed)
addGuestToWaitingForApproval(guest, liveMeeting.guestsWaiting)
notifyModeratorsOfGuestWaiting(Vector(guest), liveMeeting.users2x, liveMeeting.props.meetingProp.intId)
case GuestStatus.DENY =>

View File

@ -51,7 +51,7 @@ class GuestsWaiting {
def setGuestPolicy(policy: GuestPolicy) = guestPolicy = policy
}
case class GuestWaiting(intId: String, name: String, role: String, guest: Boolean, authenticated: Boolean)
case class GuestWaiting(intId: String, name: String, role: String, guest: Boolean, avatar: String, authenticated: Boolean)
case class GuestPolicy(policy: String, setBy: String)
object GuestPolicyType {

View File

@ -27,7 +27,7 @@ import java.util.*;
* Source:<br>
* Phoenix: An Interactive Curve Design System Based on the Automatic Fitting
* of Hand-Sketched Curves.<br>
* © Copyright by Philip J. Schneider 1988.<br>
* Copyright (c) by Philip J. Schneider 1988.<br>
* A thesis submitted in partial fulfillment of the requirements for the degree
* of Master of Science, University of Washington.
* <p>
@ -238,7 +238,7 @@ public class Bezier {
* @param digitizedPoints Digitized points
* @param maxAngle maximal angle in radians between the current point and its
* predecessor and successor up to which the point does not break the
* digitized list into segments. Recommended value 44° = 44 * 180d / Math.PI
* digitized list into segments. Recommended value 44 deg = 44 * 180d / Math.PI
* @return Segments of digitized points, each segment having less than maximal
* angle between points.
*/

View File

@ -44,7 +44,7 @@ object MsgBuilder {
val envelope = BbbCoreEnvelope(GetGuestsWaitingApprovalRespMsg.NAME, routing)
val header = BbbClientMsgHeader(GetGuestsWaitingApprovalRespMsg.NAME, meetingId, userId)
val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.authenticated))
val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.avatar, g.authenticated))
val body = GetGuestsWaitingApprovalRespMsgBody(guestsWaiting)
val event = GetGuestsWaitingApprovalRespMsg(header, body)
@ -56,7 +56,7 @@ object MsgBuilder {
val envelope = BbbCoreEnvelope(GuestsWaitingForApprovalEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(GuestsWaitingForApprovalEvtMsg.NAME, meetingId, userId)
val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.authenticated))
val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.avatar, g.authenticated))
val body = GuestsWaitingForApprovalEvtMsgBody(guestsWaiting)
val event = GuestsWaitingForApprovalEvtMsg(header, body)

View File

@ -20,13 +20,13 @@ trait FakeTestData {
val guest1 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.WEBRTC, muted = false,
talking = false, listenOnly = false)
Users2x.add(liveMeeting.users2x, guest1)
val guestWait1 = GuestWaiting(guest1.intId, guest1.name, guest1.role, guest1.guest, guest1.authed)
val guestWait1 = GuestWaiting(guest1.intId, guest1.name, guest1.role, guest1.guest, "", guest1.authed)
GuestsWaiting.add(liveMeeting.guestsWaiting, guestWait1)
val guest2 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.FLASH, muted = false,
talking = false, listenOnly = false)
Users2x.add(liveMeeting.users2x, guest2)
val guestWait2 = GuestWaiting(guest2.intId, guest2.name, guest2.role, guest2.guest, guest2.authed)
val guestWait2 = GuestWaiting(guest2.intId, guest2.name, guest2.role, guest2.guest, "", guest2.authed)
GuestsWaiting.add(liveMeeting.guestsWaiting, guestWait2)
val vu1 = FakeUserGenerator.createFakeVoiceOnlyUser(CallingWith.PHONE, muted = false, talking = false, listenOnly = false)

View File

@ -19,7 +19,7 @@ case class GetGuestsWaitingApprovalRespMsg(
body: GetGuestsWaitingApprovalRespMsgBody
) extends BbbCoreMsg
case class GetGuestsWaitingApprovalRespMsgBody(guests: Vector[GuestWaitingVO])
case class GuestWaitingVO(intId: String, name: String, role: String, guest: Boolean, authenticated: Boolean)
case class GuestWaitingVO(intId: String, name: String, role: String, guest: Boolean, avatar: String, authenticated: Boolean)
/**
* Message sent to client for list of guest waiting for approval. This is sent when

View File

@ -923,7 +923,7 @@ public class MeetingService implements MessageListener {
} else {
if (message.userId.startsWith("v_")) {
// A dial-in user joined the meeting. Dial-in users by convention has userId that starts with "v_".
User vuser = new User(message.userId, message.userId, message.name, "DIAL-IN-USER", "no-avatar-url",
User vuser = new User(message.userId, message.userId, message.name, "DIAL-IN-USER", "",
true, GuestPolicy.ALLOW, "DIAL-IN");
vuser.setVoiceJoined(true);
m.userJoined(vuser);

View File

@ -78,6 +78,7 @@ public class ParamsProcessorUtil {
private Boolean moderatorsJoinViaHTML5Client;
private Boolean attendeesJoinViaHTML5Client;
private Boolean allowRequestsWithoutSession;
private Boolean useDefaultAvatar = false;
private String defaultAvatarURL;
private String defaultConfigURL;
private String defaultGuestPolicy;
@ -464,6 +465,8 @@ public class ParamsProcessorUtil {
externalMeetingId = externalHash + "-" + timeStamp;
}
String avatarURL = useDefaultAvatar ? defaultAvatarURL : "";
// Create the meeting with all passed in parameters.
Meeting meeting = new Meeting.Builder(externalMeetingId,
internalMeetingId, createTime).withName(meetingName)
@ -474,7 +477,7 @@ public class ParamsProcessorUtil {
.withBannerText(bannerText).withBannerColor(bannerColor)
.withTelVoice(telVoice).withWebVoice(webVoice)
.withDialNumber(dialNumber)
.withDefaultAvatarURL(defaultAvatarURL)
.withDefaultAvatarURL(avatarURL)
.withAutoStartRecording(autoStartRec)
.withAllowStartStopRecording(allowStartStoptRec)
.withWebcamsOnlyForModerator(webcamsOnlyForMod)
@ -952,6 +955,10 @@ public class ParamsProcessorUtil {
this.webcamsOnlyForModerator = webcamsOnlyForModerator;
}
public void setUseDefaultAvatar(Boolean value) {
this.useDefaultAvatar = value;
}
public void setdefaultAvatarURL(String url) {
this.defaultAvatarURL = url;
}

View File

@ -3,7 +3,7 @@ bbb-webhooks
This is a node.js application that listens for all events on BigBlueButton and sends POST requests with details about these events to hooks registered via an API. A hook is any external URL that can receive HTTP POST requests.
You can read the full documentation at: http://docs.bigbluebutton.org/labs/webhooks.html
You can read the full documentation at: https://docs.bigbluebutton.org/dev/webhooks.html
Development

View File

@ -1,83 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<title>Guest Lobby</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<style></style>
<script src="lib/jquery-2.1.1.min.js" type="text/javascript"></script>
<script type="text/javascript">
function updateMessage(message) {
$('#content > p').html(message);
}
function findSessionToken() {
return location.search
.substr(1)
.split('&')
.find(function(item) {
return item.split('=')[0] === 'sessionToken'
});
};
function fetchGuestWait(sessionToken) {
const GUEST_WAIT_ENDPOINT = '/bigbluebutton/api/guestWait';
return $.get(GUEST_WAIT_ENDPOINT, sessionToken.concat('&redirect=false'));
};
function pollGuestStatus(token, attempt, limit, everyMs) {
setTimeout(function() {
var REDIRECT_STATUSES = ['ALLOW', 'DENY'];
if (attempt >= limit) {
updateMessage('TIMEOUT_MESSAGE_HERE');
return;
}
fetchGuestWait(token).always(function(data) {
console.log("data=" + JSON.stringify(data));
var status = data.response.guestStatus;
if (REDIRECT_STATUSES.includes(status)) {
window.location = data.response.url;
return;
}
return pollGuestStatus(token, attempt + 1, limit, everyMs);
})
}, everyMs);
};
window.onload = function() {
try {
var ATTEMPT_EVERY_MS = 5000;
var ATTEMPT_LIMIT = 100;
var sessionToken = findSessionToken();
if(!sessionToken) {
updateMessage('NO_SESSION_TOKEN_MESSAGE');
return;
}
pollGuestStatus(sessionToken, 0, ATTEMPT_LIMIT, ATTEMPT_EVERY_MS);
} catch (e) {
console.error(e);
updateMessage('GENERIC_ERROR_MESSAGE');
}
};
</script>
</head>
<body>
<div id="banner"></div>
<div id="content">
<p>Please wait for a moderator to approve you joining the meeting.</p>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -11,6 +11,7 @@ import Redis from './redis';
import setMinBrowserVersions from './minBrowserVersion';
let guestWaitHtml = '';
const AVAILABLE_LOCALES = fs.readdirSync('assets/app/locales');
const FALLBACK_LOCALES = JSON.parse(Assets.getText('config/fallbackLocales.json'));
@ -217,6 +218,21 @@ WebApp.connectHandlers.use('/useragent', (req, res) => {
res.end(response);
});
WebApp.connectHandlers.use('/guestWait', (req, res) => {
if (!guestWaitHtml) {
try {
guestWaitHtml = Assets.getText('static/guest-wait/guest-wait.html');
} catch (e) {
Logger.warn(`Could not process guest wait html file: ${e}`);
}
}
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(guestWaitHtml);
});
export const eventEmitter = Redis.emitter;
export const redisPubSub = Redis;

View File

@ -116,7 +116,7 @@ export default injectIntl(withTracker(({ intl }) => {
const messagesFormated = messagesBeforeWelcomeMsg
.concat(welcomeMsg)
.concat((amIModerator && modOnlyMessage) || [])
.concat((amIModerator && modOnlyMessage) ? moderatorMsg : [])
.concat(messagesAfterWelcomeMsg);
messages = messagesFormated.sort((a, b) => (a.time - b.time));

View File

@ -126,6 +126,7 @@ class MessageListItem extends Component {
className={styles.avatar}
color={user.color}
moderator={user.isModerator}
avatar={user.avatar}
>
{user.name.toLowerCase().slice(0, 2)}
</UserAvatar>

View File

@ -50,9 +50,10 @@ const mapGroupMessage = (message) => {
};
if (message.sender && message.sender.id !== SYSTEM_CHAT_TYPE) {
const sender = Users.findOne({ userId: message.sender.id }, { fields: { role: 1 } });
const sender = Users.findOne({ userId: message.sender.id }, { fields: { avatar: 1, role: 1 } });
const mappedSender = {
avatar: sender?.avatar,
color: message.color,
isModerator: sender?.role === ROLE_MODERATOR,
name: message.sender.name,

View File

@ -84,8 +84,9 @@ class ConnectionStatusComponent extends PureComponent {
<div className={styles.left}>
<div className={styles.avatar}>
<UserAvatar
className={styles.icon}
className={cx({ [styles.initials]: conn.avatar.length === 0 })}
you={conn.you}
avatar={conn.avatar}
moderator={conn.moderator}
color={conn.color}
>

View File

@ -82,7 +82,7 @@
justify-content: center;
align-items: center;
.icon {
.initials {
min-width: 2.25rem;
height: 2.25rem;
}

View File

@ -87,6 +87,7 @@ const getConnectionStatus = () => {
userId: 1,
name: 1,
role: 1,
avatar: 1,
color: 1,
connectionStatus: 1,
},
@ -96,6 +97,7 @@ const getConnectionStatus = () => {
userId,
name,
role,
avatar,
color,
connectionStatus: userStatus,
} = user;
@ -105,6 +107,7 @@ const getConnectionStatus = () => {
if (status) {
result.push({
name,
avatar,
offline: userStatus === 'offline',
you: Auth.userID === userId,
moderator: role === ROLE_MODERATOR,

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import _ from 'lodash';
import cx from 'classnames';
import Icon from '/imports/ui/components/icon/component';
@ -18,7 +19,13 @@ const defaultProps = {
tabIndex: 0,
};
export default class DropdownListItem extends Component {
const messages = defineMessages({
activeAriaLabel: {
id: 'app.dropdown.list.item.activeLabel',
},
});
class DropdownListItem extends Component {
constructor(props) {
super(props);
this.labelID = _.uniqueId('dropdown-item-label-');
@ -38,9 +45,12 @@ export default class DropdownListItem extends Component {
render() {
const {
id, label, description, children, injectRef, tabIndex, onClick, onKeyDown,
className, style,
className, style, intl,
} = this.props;
const isSelected = className && className.includes('emojiSelected');
const _label = isSelected ? `${label} (${intl.formatMessage(messages.activeAriaLabel)})` : label;
return (
<li
id={id}
@ -59,8 +69,8 @@ export default class DropdownListItem extends Component {
children || this.renderDefault()
}
{
label ?
(<span id={this.labelID} key="labelledby" hidden>{label}</span>)
label
? (<span id={this.labelID} key="labelledby" hidden>{_label}</span>)
: null
}
<span id={this.descID} key="describedby" hidden>{description}</span>
@ -69,5 +79,7 @@ export default class DropdownListItem extends Component {
}
}
export default injectIntl(DropdownListItem);
DropdownListItem.propTypes = propTypes;
DropdownListItem.defaultProps = defaultProps;

View File

@ -10,6 +10,10 @@ const intlMessages = defineMessages({
id: 'app.fullscreenButton.label',
description: 'Fullscreen label',
},
fullscreenUndoButton: {
id: 'app.fullscreenUndoButton.label',
description: 'Undo fullscreen label',
},
});
const propTypes = {
@ -47,10 +51,17 @@ const FullscreenButtonComponent = ({
}) => {
if (isIphone) return null;
const formattedLabel = intl.formatMessage(
const formattedLabel = (isFullscreen) => {
return(isFullscreen ?
intl.formatMessage(
intlMessages.fullscreenUndoButton,
({ 0: elementName || '' }),
) :
intl.formatMessage(
intlMessages.fullscreenButton,
({ 0: elementName || '' }),
);
));
};
const wrapperClassName = cx({
[styles.wrapper]: true,
@ -67,7 +78,7 @@ const FullscreenButtonComponent = ({
icon={!isFullscreen ? 'fullscreen' : 'exit_fullscreen'}
size="sm"
onClick={() => handleToggleFullScreen(fullscreenRef)}
label={formattedLabel}
label={formattedLabel(isFullscreen)}
hideLabel
className={cx(styles.button, styles.fullScreenButton, className)}
data-test="presentationFullscreenButton"

View File

@ -11,6 +11,7 @@ import ZoomTool from './zoom-tool/component';
import FullscreenButtonContainer from '../../fullscreen-button/container';
import TooltipContainer from '/imports/ui/components/tooltip/container';
import QuickPollDropdownContainer from '/imports/ui/components/actions-bar/quick-poll-dropdown/container';
import FullscreenService from '/imports/ui/components/fullscreen-button/service';
import KEY_CODES from '/imports/utils/keyCodes';
const intlMessages = defineMessages({
@ -101,6 +102,7 @@ class PresentationToolbar extends PureComponent {
switchSlide(event) {
const { target, which } = event;
const isBody = target.nodeName === 'BODY';
const { fullscreenRef } = this.props;
if (isBody) {
switch (which) {
@ -112,6 +114,9 @@ class PresentationToolbar extends PureComponent {
case KEY_CODES.PAGE_DOWN:
this.nextSlideHandler();
break;
case KEY_CODES.ENTER:
FullscreenService.toggleFullScreen(fullscreenRef);
break;
default:
}
}

View File

@ -14,6 +14,8 @@ const propTypes = {
voice: PropTypes.bool,
noVoice: PropTypes.bool,
color: PropTypes.string,
emoji: PropTypes.bool,
avatar: PropTypes.string,
className: PropTypes.string,
};
@ -26,6 +28,8 @@ const defaultProps = {
voice: false,
noVoice: false,
color: '#000',
emoji: false,
avatar: '',
className: null,
};
@ -38,6 +42,8 @@ const UserAvatar = ({
listenOnly,
color,
voice,
emoji,
avatar,
noVoice,
className,
}) => (
@ -60,14 +66,27 @@ const UserAvatar = ({
>
<div className={cx({
[styles.talking]: (talking && !muted),
[styles.talking]: (talking && !muted && avatar.length === 0),
})}
/>
{avatar.length !== 0 && !emoji
? (
<div className={styles.image}>
<img
className={cx(styles.img, {
[styles.circle]: !moderator,
[styles.square]: moderator,
})}
src={avatar}
/>
</div>
) : (
<div className={styles.content}>
{children}
</div>
)
}
</div>
);

View File

@ -13,7 +13,8 @@
.avatar {
position: relative;
padding-bottom: 2rem;
height: 2.25rem;
min-width: 2.25rem;
border-radius: 50%;
text-align: center;
font-size: .85rem;
@ -166,6 +167,25 @@
@include indicatorStyles();
}
.image {
display: flex;
height: 2rem;
width: 2rem;
.img {
object-fit: cover;
overflow: hidden;
}
.circle {
border-radius: 50%;
}
.square {
border-radius: 3px;
}
}
.content {
color: var(--user-avatar-text);
top: 50%;

View File

@ -12,11 +12,14 @@ const defaultProps = {
};
const ChatAvatar = (props) => {
const { color, name, isModerator } = props;
const {
color, name, avatar, isModerator,
} = props;
return (
<UserAvatar
moderator={isModerator}
avatar={avatar}
color={color}
>
{name.toLowerCase().slice(0, 2)}

View File

@ -96,6 +96,7 @@ const ChatListItem = (props) => {
<ChatAvatar
isModerator={chat.isModerator}
color={chat.color}
avatar={chat.avatar}
name={chat.name.toLowerCase().slice(0, 2)}
/>
)}

View File

@ -538,6 +538,8 @@ class UserDropdown extends PureComponent {
voice={voiceUser.isVoiceUser}
noVoice={!voiceUser.isVoiceUser}
color={user.color}
emoji={user.emoji !== 'none'}
avatar={user.avatar}
>
{
userInBreakout

View File

@ -623,7 +623,7 @@ class VideoPreview extends Component {
: (
<video
id="preview"
data-test="videoPreview"
data-test={this.mirrorOwnWebcam ? 'mirroredVideoPreview' : 'videoPreview'}
className={cx({
[styles.preview]: true,
[styles.mirroredVideo]: this.mirrorOwnWebcam,

View File

@ -168,7 +168,7 @@ class VideoListItem extends Component {
>
<video
muted
data-test="videoContainer"
data-test={this.mirrorOwnWebcam ? 'mirroredVideoContainer' : 'videoContainer'}
className={cx({
[styles.media]: true,
[styles.cursorGrab]: !webcamDraggableState.dragging

View File

@ -66,13 +66,14 @@ const getNameInitials = (name) => {
return nameInitials.replace(/^\w/, c => c.toUpperCase());
}
const renderGuestUserItem = (name, color, handleAccept, handleDeny, role, sequence, userId, intl) => (
const renderGuestUserItem = (name, color, handleAccept, handleDeny, role, sequence, userId, avatar, intl) => (
<div key={`userlist-item-${userId}`} className={styles.listItem}>
<div key={`user-content-container-${userId}`} className={styles.userContentContainer}>
<div key={`user-avatar-container-${userId}`} className={styles.userAvatar}>
<UserAvatar
key={`user-avatar-${userId}`}
moderator={role === 'MODERATOR'}
avatar={avatar}
color={color}
>
{getNameInitials(name)}
@ -123,6 +124,7 @@ const renderPendingUsers = (message, usersArray, action, intl) => {
user.role,
idx + 1,
user.intId,
user.avatar,
intl,
))}
</div>

View File

@ -530,6 +530,8 @@ private:
version: 50
- browser: electron
version: [0, 36]
- browser: SamsungInternet
version: 10
- browser: YandexBrowser
version: 19

View File

@ -198,8 +198,8 @@
"app.presentationUploder.conversion.pdfHasBigPage": "We could not convert the PDF file, please try optimizing it",
"app.presentationUploder.conversion.timeout": "Ops, the conversion took too long",
"app.presentationUploder.conversion.pageCountFailed": "Failed to determine the number of pages.",
"app.presentationUploder.isDownloadableLabel": "Do not allow presentation to be downloaded",
"app.presentationUploder.isNotDownloadableLabel": "Allow presentation to be downloaded",
"app.presentationUploder.isDownloadableLabel": "Presentation download is not allowed - click to allow presentation to be downloaded",
"app.presentationUploder.isNotDownloadableLabel": "Presentation download is allowed - click to disallow presentation to be downloaded",
"app.presentationUploder.removePresentationLabel": "Remove presentation",
"app.presentationUploder.setAsCurrentPresentation": "Set presentation as current",
"app.presentationUploder.tableHeading.filename": "File name",
@ -373,7 +373,7 @@
"app.actionsBar.emojiMenu.statusTriggerLabel": "Set status",
"app.actionsBar.emojiMenu.awayLabel": "Away",
"app.actionsBar.emojiMenu.awayDesc": "Change your status to away",
"app.actionsBar.emojiMenu.raiseHandLabel": "Raise",
"app.actionsBar.emojiMenu.raiseHandLabel": "Raise hand",
"app.actionsBar.emojiMenu.raiseHandDesc": "Raise your hand to ask a question",
"app.actionsBar.emojiMenu.neutralLabel": "Undecided",
"app.actionsBar.emojiMenu.neutralDesc": "Change your status to undecided",
@ -488,6 +488,7 @@
"app.modal.newTab": "(opens new tab)",
"app.modal.confirm.description": "Saves changes and closes the modal",
"app.dropdown.close": "Close",
"app.dropdown.list.item.activeLabel": "Active",
"app.error.400": "Bad Request",
"app.error.401": "Unauthorized",
"app.error.403": "You have been removed from the meeting",
@ -619,6 +620,7 @@
"app.video.pagination.nextPage": "See next videos",
"app.video.clientDisconnected": "Webcam cannot be shared due to connection issues",
"app.fullscreenButton.label": "Make {0} fullscreen",
"app.fullscreenUndoButton.label": "Undo {0} fullscreen",
"app.deskshare.iceConnectionStateError": "Connection failed when sharing screen (ICE error 1108)",
"app.sfu.mediaServerConnectionError2000": "Unable to connect to media server (error 2000)",
"app.sfu.mediaServerOffline2001": "Media server is offline. Please try again later (error 2001)",

View File

@ -0,0 +1,155 @@
<!DOCTYPE html>
<html>
<head>
<title>Guest Lobby</title>
<meta charset="UTF-8">
<style>
:root{
--enableAnimation: 1;
}
body {
display: flex;
justify-content: center;
align-items: center;
background: #06172A;
height: 100vh;
margin: 0;
}
#content {
text-align: center;
color: white;
font-weight: bold;
font-size: 24px;
font-family: arial, sans-serif;
}
.spinner {
margin: 20px auto;
}
.spinner .bounce1 {
animation-delay: -0.32s;
}
.spinner .bounce2 {
animation-delay: -0.16s;
}
.spinner > div {
width: 30px;
height: 30px;
background-color: rgb(255, 255, 255);
display: inline-block;
border-radius: 100%;
animation: sk-bouncedelay calc(var(--enableAnimation) * 1.4s) infinite ease-in-out both;
}
@-webkit-keyframes sk-bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0)
}
40% {
-webkit-transform: scale(1.0)
}
}
@keyframes sk-bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1.0);
transform: scale(1.0);
}
}
</style>
<script type="text/javascript">
function updateMessage(message) {
document.querySelector('#content > p').innerHTML = message;
}
function findSessionToken() {
return location.search
.substr(1)
.split('&')
.find(function(item) {
return item.split('=')[0] === 'sessionToken'
});
};
function fetchGuestWait(sessionToken) {
const GUEST_WAIT_ENDPOINT = '/bigbluebutton/api/guestWait';
const urlTest = new URL(`${window.location.origin}${GUEST_WAIT_ENDPOINT}`);
const concatedParams = sessionToken.concat('&redirect=false');
urlTest.search = concatedParams;
return fetch(urlTest, {method: 'get'});
};
function pollGuestStatus(token, attempt, limit, everyMs) {
setTimeout(function() {
var REDIRECT_STATUSES = ['ALLOW', 'DENY'];
if (attempt >= limit) {
disableAnimation();
updateMessage('No respons from Moderator');
return;
}
fetchGuestWait(token)
.then(async (resp) => await resp.json())
.then((data) => {
console.log("data=" + JSON.stringify(data));
var status = data.response.guestStatus;
if (REDIRECT_STATUSES.includes(status)) {
disableAnimation();
window.location = data.response.url;
return;
}
return pollGuestStatus(token, attempt + 1, limit, everyMs);
});
}, everyMs);
};
function enableAnimation(){
document.documentElement.style.setProperty('--enableAnimation', 1);
}
function disableAnimation() {
document.documentElement.style.setProperty('--enableAnimation', 0);
}
window.onload = function() {
enableAnimation();
try {
var ATTEMPT_EVERY_MS = 5000;
var ATTEMPT_LIMIT = 100;
var sessionToken = findSessionToken();
if(!sessionToken) {
disableAnimation()
updateMessage('No session Token received');
return;
}
pollGuestStatus(sessionToken, 0, ATTEMPT_LIMIT, ATTEMPT_EVERY_MS);
} catch (e) {
disableAnimation();
console.error(e);
updateMessage('Error: more details in the console');
}
};
</script>
</head>
<body>
<div id="content">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<p>Please wait for a moderator to approve you joining the meeting.</p>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -10,6 +10,7 @@ const userTest = require('./user.obj');
const virtualizedListTest = require('./virtualizedlist.obj');
const webcamTest = require('./webcam.obj');
const whiteboardTest = require('./whiteboard.obj');
const webcamLayout = require('./webcamlayout.obj');
process.setMaxListeners(Infinity);
@ -25,3 +26,4 @@ describe('User', userTest);
describe('Virtualized List', virtualizedListTest);
describe('Webcam', webcamTest);
describe('Whiteboard', whiteboardTest);
describe('Webcam Layout', webcamLayout);

View File

@ -252,12 +252,11 @@ html5ClientUrl=${bigbluebutton.web.serverURL}/html5client/join
# The url for where the guest will poll if approved to join or not.
defaultGuestWaitURL=${bigbluebutton.web.serverURL}/client/guest-wait.html
defaultGuestWaitURL=${bigbluebutton.web.serverURL}/html5client/guestWait
# The default avatar image to display if nothing is passed on the JOIN API (avatarURL)
# call. This avatar is displayed if the user isn't sharing the webcam and
# the option (displayAvatar) is enabled in config.xml
defaultAvatarURL=${bigbluebutton.web.serverURL}/client/avatar.png
# The default avatar image to display.
useDefaultAvatar=false
defaultAvatarURL=${bigbluebutton.web.serverURL}/html5client/resources/images/avatar.png
# The URL of the default configuration
defaultConfigURL=${bigbluebutton.web.serverURL}/client/conf/config.xml

View File

@ -133,6 +133,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<property name="autoStartRecording" value="${autoStartRecording}"/>
<property name="allowStartStopRecording" value="${allowStartStopRecording}"/>
<property name="webcamsOnlyForModerator" value="${webcamsOnlyForModerator}"/>
<property name="useDefaultAvatar" value="${useDefaultAvatar}"/>
<property name="defaultAvatarURL" value="${defaultAvatarURL}"/>
<property name="defaultConfigURL" value="${defaultConfigURL}"/>
<property name="defaultGuestPolicy" value="${defaultGuestPolicy}"/>