Merge remote-tracking branch 'upstream/develop' into connection-manager
This commit is contained in:
commit
dadca62741
@ -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 =>
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
@ -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;
|
||||
|
@ -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));
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -82,7 +82,7 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
.initials {
|
||||
min-width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
intlMessages.fullscreenButton,
|
||||
({ 0: elementName || '' }),
|
||||
);
|
||||
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"
|
||||
|
@ -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:
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
})}
|
||||
/>
|
||||
|
||||
|
||||
<div className={styles.content}>
|
||||
{children}
|
||||
</div>
|
||||
{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>
|
||||
);
|
||||
|
||||
|
@ -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%;
|
||||
|
@ -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)}
|
||||
|
@ -96,6 +96,7 @@ const ChatListItem = (props) => {
|
||||
<ChatAvatar
|
||||
isModerator={chat.isModerator}
|
||||
color={chat.color}
|
||||
avatar={chat.avatar}
|
||||
name={chat.name.toLowerCase().slice(0, 2)}
|
||||
/>
|
||||
)}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -530,6 +530,8 @@ private:
|
||||
version: 50
|
||||
- browser: electron
|
||||
version: [0, 36]
|
||||
- browser: SamsungInternet
|
||||
version: 10
|
||||
- browser: YandexBrowser
|
||||
version: 19
|
||||
|
||||
|
@ -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)",
|
||||
|
155
bigbluebutton-html5/private/static/guest-wait/guest-wait.html
Executable file
155
bigbluebutton-html5/private/static/guest-wait/guest-wait.html
Executable 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>
|
BIN
bigbluebutton-html5/public/resources/images/avatar.png
Normal file
BIN
bigbluebutton-html5/public/resources/images/avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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}"/>
|
||||
|
Loading…
Reference in New Issue
Block a user