Merge branch 'develop' into issue-9219

This commit is contained in:
Tainan Felipe 2020-07-28 13:28:32 -03:00 committed by GitHub
commit e64c0e1c62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
234 changed files with 11422 additions and 1530 deletions

33
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,33 @@
<!--
PLEASE READ THIS MESSAGE.
HOW TO WRITE A GOOD PULL REQUEST?
- Make it small.
- Do only one thing.
- Avoid re-formatting.
- Make sure the code builds and works.
- Write useful descriptions and titles.
- Address review comments in terms of additional commits.
- Do not amend/squash existing ones unless the PR is trivial.
- Read the contributing guide: https://docs.bigbluebutton.org/support/faq.html#bigbluebutton-development-process
- Sign and send the Contributor License Agreement: https://docs.bigbluebutton.org/support/faq.html#why-do-i-need-to-sign-a-contributor-license-agreement-to-contribute-source-code
-->
### What does this PR do?
<!-- A brief description of each change being made with this pull request. -->
### Closes Issue(s)
<!-- List here all the issues closed by this pull request. Use keyword `closes` before each issue number -->
### Motivation
<!-- What inspired you to submit this pull request? -->
### More
<!-- Anything else we should know when reviewing? -->
- [ ] Added/updated documentation

17
SECURITY.md Normal file
View File

@ -0,0 +1,17 @@
# Security Policy
## Supported Versions
We actively support BigBlueButton through the community forums and through security updates.
| Version | Supported |
| ------- | ------------------ |
| 2.0.x (or earlier) | :x: |
| 2.2.x | :white_check_mark: |
| 2.3-dev | :white_check_mark: |
## Reporting a Vulnerability
If you believe you have found a security vunerability in BigBlueButton please let us know directly by e-mailing security@bigbluebutton.org with as much detail as possible.
Regards,... [BigBlueButton Team](https://docs.bigbluebutton.org/support/faq.html#bigbluebutton-committer)

View File

@ -26,7 +26,7 @@ object UsersApp {
u <- RegisteredUsers.findWithUserId(userId, liveMeeting.registeredUsers)
} yield {
RegisteredUsers.eject(u.id, liveMeeting.registeredUsers, u.id)
RegisteredUsers.eject(u.id, liveMeeting.registeredUsers, false)
val event = MsgBuilder.buildGuestWaitingLeftEvtMsg(liveMeeting.props.meetingProp.intId, u.id)
outGW.send(event)

View File

@ -21,7 +21,7 @@ object Dependencies {
val apacheHttpAsync = "4.1.4"
// Office and document conversion
val jodConverter = "4.2.1"
val jodConverter = "4.3.0"
val apachePoi = "4.1.2"
val nuProcess = "1.2.4"
val libreOffice = "5.4.2"

View File

@ -36,6 +36,10 @@ public class ApiErrors {
errors.add(new String[] {"NotUniqueMeetingID", "A meeting already exists with that meeting ID. Please use a different meeting ID."});
}
public void nonUniqueVoiceBridgeError() {
errors.add(new String[] {"nonUniqueVoiceBridge", "The selected voice bridge is already in use."});
}
public void invalidMeetingIdError() {
errors.add(new String[] {"invalidMeetingId", "The meeting ID that you supplied did not match any existing meetings"});
}

View File

@ -277,8 +277,10 @@ public class MeetingService implements MessageListener {
public synchronized boolean createMeeting(Meeting m) {
String internalMeetingId = paramsProcessorUtil.convertToInternalMeetingId(m.getExternalId());
Meeting existing = getNotEndedMeetingWithId(internalMeetingId);
if (existing == null) {
Meeting existingId = getNotEndedMeetingWithId(internalMeetingId);
Meeting existingTelVoice = getNotEndedMeetingWithTelVoice(m.getTelVoice());
Meeting existingWebVoice = getNotEndedMeetingWithWebVoice(m.getWebVoice());
if (existingId == null && existingTelVoice == null && existingWebVoice == null) {
meetings.put(m.getInternalId(), m);
handle(new CreateMeeting(m));
return true;
@ -437,6 +439,32 @@ public class MeetingService implements MessageListener {
return null;
}
public Meeting getNotEndedMeetingWithTelVoice(String telVoice) {
if (telVoice == null)
return null;
for (Map.Entry<String, Meeting> entry : meetings.entrySet()) {
Meeting m = entry.getValue();
if (telVoice.equals(m.getTelVoice())) {
if (!m.isForciblyEnded())
return m;
}
}
return null;
}
public Meeting getNotEndedMeetingWithWebVoice(String webVoice) {
if (webVoice == null)
return null;
for (Map.Entry<String, Meeting> entry : meetings.entrySet()) {
Meeting m = entry.getValue();
if (webVoice.equals(m.getWebVoice())) {
if (!m.isForciblyEnded())
return m;
}
}
return null;
}
public Boolean validateTextTrackSingleUseToken(String recordId, String caption, String token) {
return recordingService.validateTextTrackSingleUseToken(recordId, caption, token);
}

View File

@ -362,7 +362,7 @@ public class Meeting {
} else if (GuestPolicy.ALWAYS_DENY.equals(guestPolicy)) {
return GuestPolicy.DENY;
} else if (GuestPolicy.ASK_MODERATOR.equals(guestPolicy)) {
if (guest || (!ROLE_MODERATOR.equals(role) && !authned)) {
if (guest || (!ROLE_MODERATOR.equals(role) && authned)) {
return GuestPolicy.WAIT ;
}
return GuestPolicy.ALLOW;

View File

@ -13,7 +13,6 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;

View File

@ -20,21 +20,33 @@
package org.bigbluebutton.presentation.imp;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import com.sun.org.apache.xerces.internal.impl.xs.opti.DefaultDocument;
import org.apache.commons.io.FilenameUtils;
import org.bigbluebutton.presentation.UploadedPresentation;
import org.jodconverter.OfficeDocumentConverter;
import org.jodconverter.core.document.DefaultDocumentFormatRegistry;
import org.jodconverter.core.document.DocumentFormat;
import org.jodconverter.core.job.AbstractConverter;
import org.jodconverter.local.LocalConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
public class Office2PdfPageConverter {
public abstract class Office2PdfPageConverter {
private static Logger log = LoggerFactory.getLogger(Office2PdfPageConverter.class);
public boolean convert(File presentationFile, File output, int page, UploadedPresentation pres,
final OfficeDocumentConverter converter){
public static boolean convert(File presentationFile, File output, int page, UploadedPresentation pres,
LocalConverter converter){
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try {
Map<String, Object> logData = new HashMap<>();
logData.put("meetingId", pres.getMeetingId());
@ -46,7 +58,15 @@ public class Office2PdfPageConverter {
String logStr = gson.toJson(logData);
log.info(" --analytics-- data={}", logStr);
converter.convert(presentationFile, output);
final DocumentFormat sourceFormat = DefaultDocumentFormatRegistry.getFormatByExtension(
FilenameUtils.getExtension(presentationFile.getName()));
inputStream = new FileInputStream(presentationFile);
outputStream = new FileOutputStream(output);
converter.convert(inputStream).as(sourceFormat).to(outputStream).as(DefaultDocumentFormatRegistry.PDF).execute();
outputStream.flush();
if (output.exists()) {
return true;
} else {
@ -74,6 +94,22 @@ public class Office2PdfPageConverter {
String logStr = gson.toJson(logData);
log.error(" --analytics-- data={}", logStr, e);
return false;
} finally {
if(inputStream!=null) {
try {
inputStream.close();
} catch(Exception e) {
}
}
if(outputStream!=null) {
try {
outputStream.close();
} catch(Exception e) {
}
}
}
}

View File

@ -0,0 +1,29 @@
package org.bigbluebutton.presentation.imp;
import org.jodconverter.core.office.OfficeContext;
import org.jodconverter.local.filter.Filter;
import org.jodconverter.local.filter.FilterChain;
import org.jodconverter.local.office.utils.Lo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.star.lang.XComponent;
import com.sun.star.sheet.XCalculatable;
public class OfficeDocumentConversionFilter implements Filter {
private static Logger log = LoggerFactory.getLogger(OfficeDocumentConversionFilter.class);
@Override
public void doFilter(OfficeContext context, XComponent document, FilterChain chain)
throws Exception {
log.info("Applying the OfficeDocumentConversionFilter");
Lo.qiOptional(XCalculatable.class, document).ifPresent((x) -> {
log.info("Turn AutoCalculate off");
x.enableAutomaticCalculation(false);
});
chain.doFilter(context, document);
}
}

View File

@ -20,16 +20,18 @@
package org.bigbluebutton.presentation.imp;
import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.bigbluebutton.presentation.ConversionMessageConstants;
import org.bigbluebutton.presentation.SupportedFileTypes;
import org.bigbluebutton.presentation.UploadedPresentation;
import org.jodconverter.OfficeDocumentConverter;
import org.jodconverter.office.DefaultOfficeManagerBuilder;
import org.jodconverter.office.OfficeException;
import org.jodconverter.office.OfficeManager;
import org.jodconverter.core.office.OfficeException;
import org.jodconverter.core.office.OfficeUtils;
import org.jodconverter.local.LocalConverter;
import org.jodconverter.local.office.ExternalOfficeManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -39,15 +41,15 @@ public class OfficeToPdfConversionService {
private static Logger log = LoggerFactory.getLogger(OfficeToPdfConversionService.class);
private OfficeDocumentValidator2 officeDocumentValidator;
private final OfficeManager officeManager;
private final OfficeDocumentConverter documentConverter;
private final ArrayList<ExternalOfficeManager> officeManagers;
private ExternalOfficeManager currentManager = null;
private boolean skipOfficePrecheck = false;
private int sofficeBasePort = 0;
private int sofficeManagers = 0;
private String sofficeWorkingDirBase = null;
public OfficeToPdfConversionService() {
final DefaultOfficeManagerBuilder configuration = new DefaultOfficeManagerBuilder();
configuration.setPortNumbers(8100, 8101, 8102, 8103, 8104);
officeManager = configuration.build();
documentConverter = new OfficeDocumentConverter(officeManager);
public OfficeToPdfConversionService() throws OfficeException {
officeManagers = new ArrayList<>();
}
/*
@ -116,8 +118,39 @@ public class OfficeToPdfConversionService {
private boolean convertOfficeDocToPdf(UploadedPresentation pres,
File pdfOutput) {
Office2PdfPageConverter converter = new Office2PdfPageConverter();
return converter.convert(pres.getUploadedFile(), pdfOutput, 0, pres, documentConverter);
boolean success = false;
int attempts = 0;
while(!success) {
LocalConverter documentConverter = LocalConverter
.builder()
.officeManager(currentManager)
.filterChain(new OfficeDocumentConversionFilter())
.build();
success = Office2PdfPageConverter.convert(pres.getUploadedFile(), pdfOutput, 0, pres, documentConverter);
if(!success) {
// In case of failure, try with other open Office Manager
if(++attempts != officeManagers.size()) {
// Go to next Office Manager ( if the last retry with the first one )
int currentManagerIndex = officeManagers.indexOf(currentManager);
boolean isLastManager = ( currentManagerIndex == officeManagers.size()-1 );
if(isLastManager) {
currentManager = officeManagers.get(0);
} else {
currentManager = officeManagers.get(currentManagerIndex+1);
}
} else {
// We tried to use all our office managers and it's still failing
break;
}
}
}
return success;
}
private void makePdfTheUploadedFileAndSetStepAsSuccess(UploadedPresentation pres, File pdf) {
@ -133,21 +166,66 @@ public class OfficeToPdfConversionService {
this.skipOfficePrecheck = skipOfficePrecheck;
}
public void start() {
try {
officeManager.start();
} catch (OfficeException e) {
log.error("Could not start Office Manager", e);
public void setSofficeBasePort(int sofficeBasePort) {
this.sofficeBasePort = sofficeBasePort;
}
public void setSofficeManagers(int sofficeServiceManagers) {
this.sofficeManagers = sofficeServiceManagers;
}
public void setSofficeWorkingDirBase(String sofficeWorkingDirBase) {
this.sofficeWorkingDirBase = sofficeWorkingDirBase;
}
public void start() {
log.info("Starting LibreOffice pool with " + sofficeManagers + " managers, starting from port " + sofficeBasePort);
for(int managerIndex = 0; managerIndex < sofficeManagers; managerIndex ++) {
Integer instanceNumber = managerIndex + 1; // starts at 1
try {
final File workingDir = new File(sofficeWorkingDirBase + String.format("%02d", instanceNumber));
if(!workingDir.exists()) {
workingDir.mkdir();
}
ExternalOfficeManager officeManager = ExternalOfficeManager
.builder()
.connectTimeout(2000L)
.retryInterval(500L)
.portNumber(sofficeBasePort + managerIndex)
.connectOnStart(false) // If it's true and soffice is not available, exception is thrown here ( we don't want exception here - we want the manager alive trying to reconnect )
.workingDir(workingDir)
.build();
// Workaround for jodconverter not calling makeTempDir when connectOnStart=false (issue 211)
Method method = officeManager.getClass().getSuperclass().getDeclaredMethod("makeTempDir");
method.setAccessible(true);
method.invoke(officeManager);
// End of workaround for jodconverter not calling makeTempDir
officeManager.start();
officeManagers.add(officeManager);
} catch (Exception e) {
log.error("Could not start Office Manager " + instanceNumber + ". Details: " + e.getMessage());
}
}
if (officeManagers.size() == 0) {
log.error("No office managers could be started");
return;
}
currentManager = officeManagers.get(0);
}
public void stop() {
try {
officeManager.stop();
} catch (OfficeException e) {
officeManagers.forEach(officeManager -> officeManager.stop() );
} catch (Exception e) {
log.error("Could not stop Office Manager", e);
}
}
}

View File

@ -0,0 +1,19 @@
[Unit]
Description=BigBlueButton Libre Office container %i
Requires=network.target
[Service]
Type=simple
WorkingDirectory=/tmp
ExecStart=/usr/share/bbb-libreoffice/libreoffice_container.sh %i
ExecStop=/usr/bin/docker kill bbb-libreoffice-%i
Restart=always
RestartSec=60
SuccessExitStatus=
TimeoutStopSec=30
PermissionsStartOnly=true
LimitNOFILE=1024
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,46 @@
#!/bin/bash
set -e
INSTANCE_NUMBER=$1
if [ -z "$INSTANCE_NUMBER" ]; then
INSTANCE_NUMBER=0
fi;
_kill() {
CHECK_CONTAINER=`docker inspect bbb-libreoffice-${INSTANCE_NUMBER} &> /dev/null && echo 1 || echo 0`
if [ "$CHECK_CONTAINER" = "1" ]; then
echo "Killing container"
docker kill bbb-libreoffice-${INSTANCE_NUMBER};
sleep 1
fi;
}
trap _kill SIGINT
if (($INSTANCE_NUMBER >= 1)); then
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
_kill
let PORT=8200+${INSTANCE_NUMBER}
SOFFICE_WORK_DIR="/var/tmp/soffice_"`printf "%02d\n" $INSTANCE_NUMBER`
INPUT_RULE="INPUT -i br-soffice -m state --state NEW -j DROP"
iptables -C $INPUT_RULE || iptables -I $INPUT_RULE
FORWARD_RULE="FORWARD -i br-soffice -m state --state NEW -j DROP"
iptables -C $FORWARD_RULE || iptables -I $FORWARD_RULE
docker run --network bbb-libreoffice --name bbb-libreoffice-${INSTANCE_NUMBER} -p $PORT:8000 -v${SOFFICE_WORK_DIR}:${SOFFICE_WORK_DIR} --rm bbb-libreoffice &
wait $!
else
echo ;
echo "Invalid or missing parameter INSTANCE_NUMBER"
echo " Usage: $0 INSTANCE_NUMBER"
exit 1
fi;

View File

@ -0,0 +1,29 @@
FROM openjdk:8-jre
ENV DEBIAN_FRONTEND noninteractive
RUN apt update
ARG user_id
RUN echo "User id = $user_id"
RUN addgroup --system --gid $user_id libreoffice
# We need to ensure that this user id is the same as the user bigbluebutton in the host
RUN adduser --disabled-password --system --disabled-login --shell /sbin/nologin --gid $user_id --uid $user_id libreoffice
RUN apt -y install locales-all fontconfig libxt6 libxrender1
RUN apt -y install libreoffice --no-install-recommends
RUN dpkg-reconfigure fontconfig && fc-cache -f -s -v
VOLUME ["/usr/share/fonts/"]
RUN chown libreoffice /home/libreoffice/
ADD ./bbb-libreoffice-entrypoint.sh /home/libreoffice/
RUN chown -R libreoffice /home/libreoffice/
RUN chmod 700 /home/libreoffice/bbb-libreoffice-entrypoint.sh
USER libreoffice
ENTRYPOINT ["/home/libreoffice/bbb-libreoffice-entrypoint.sh" ]

View File

@ -0,0 +1,7 @@
#!/bin/bash
## Initialize environment
/usr/lib/libreoffice/program/soffice.bin -env:UserInstallation="file:///tmp/"
## Run daemon
/usr/lib/libreoffice/program/soffice.bin --accept="socket,host=0.0.0.0,port=8000,tcpNoDelay=1;urp;StarOffice.ServiceManager" --headless --invisible --nocrashreport --nodefault --nofirststartwizard --nolockcheck --nologo --norestore -env:UserInstallation="file:///tmp/"

59
bbb-libreoffice/install.sh Executable file
View File

@ -0,0 +1,59 @@
#!/bin/bash
if [ "$EUID" -ne 0 ]; then
echo "Please run this script as root ( or with sudo )" ;
exit 1;
fi;
DOCKER_CHECK=`docker --version &> /dev/null && echo 1 || echo 0`
if [ "$DOCKER_CHECK" = "0" ]; then
echo "Docker not found";
apt update;
apt install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
apt update
apt install docker-ce -y
systemctl enable docker
systemctl start docker
systemctl status docker
else
echo "Docker already installed";
fi
IMAGE_CHECK=`docker image inspect bbb-libreoffice &> /dev/null && echo 1 || echo 0`
if [ "$IMAGE_CHECK" = "0" ]; then
echo "Docker image doesn't exists, building"
docker build -t bbb-libreoffice --build-arg user_id=`id -u bigbluebutton` docker/
else
echo "Docker image already exists";
fi
NETWORK_CHECK=`docker network inspect bbb-libreoffice &> /dev/null && echo 1 || echo 0`
if [ "$NETWORK_CHECK" = "0" ]; then
echo "Docker network doesn't exists, creating"
docker network create bbb-libreoffice -d bridge --opt com.docker.network.bridge.name=br-soffice
fi
FOLDER_CHECK=`[ -d /usr/share/bbb-libreoffice/ ] && echo 1 || echo 0`
if [ "$FOLDER_CHECK" = "0" ]; then
echo "Install folder doesn't exists, installing"
mkdir -m 755 /usr/share/bbb-libreoffice/
cp assets/libreoffice_container.sh /usr/share/bbb-libreoffice/
chmod 700 /usr/share/bbb-libreoffice/libreoffice_container.sh
chown -R root /usr/share/bbb-libreoffice/
cp assets/bbb-libreoffice.service /lib/systemd/system/bbb-libreoffice@.service
systemctl daemon-reload
for i in `seq 1 4` ; do
systemctl enable bbb-libreoffice@${i}
systemctl start bbb-libreoffice@${i}
done
else
echo "Install folder already exists"
fi;

37
bbb-libreoffice/uninstall.sh Executable file
View File

@ -0,0 +1,37 @@
#!/bin/bash
set -e
if [ "$EUID" -ne 0 ]; then
echo "Please run this script as root ( or with sudo )" ;
exit 1;
fi;
IMAGE_CHECK=`docker image inspect bbb-libreoffice 2>&1 > /dev/null && echo 1 || echo 0`
if [ "$IMAGE_CHECK" = "1" ]; then
echo "Stopping services"
systemctl --no-pager --no-legend --value --state=running | grep bbb-libreoffice | awk -F '.service' '{print $1}' | xargs --no-run-if-empty -n 1 systemctl stop
echo "Removing image"
docker image rm bbb-libreoffice
fi
FOLDER_CHECK=`[ -d /usr/share/bbb-libreoffice/ ] && echo 1 || echo 0`
if [ "$FOLDER_CHECK" = "1" ]; then
echo "Stopping services"
systemctl --no-pager --no-legend --value --state=running | grep bbb-libreoffice | awk -F '.service' '{print $1}' | xargs --no-run-if-empty -n 1 systemctl stop
echo "Removing install folder"
rm -rf /usr/share/bbb-libreoffice/
echo "Removing service definitions"
rm /lib/systemd/system/bbb-libreoffice@.service
find /etc/systemd/ | grep bbb-libreoffice | xargs --no-run-if-empty -n 1 -I __ rm __
systemctl daemon-reload
fi;
NETWORK_CHECK=`docker network inspect bbb-libreoffice &> /dev/null && echo 1 || echo 0`
if [ "$NETWORK_CHECK" = "1" ]; then
echo "Removing docker network"
docker network remove bbb-libreoffice
fi

View File

@ -2,7 +2,7 @@ FROM java:8-jdk AS builder
RUN mkdir -p /root/tools \
&& cd /root/tools \
&& wget http://services.gradle.org/distributions/gradle-2.12-bin.zip \
&& wget https://services.gradle.org/distributions/gradle-2.12-bin.zip \
&& unzip gradle-2.12-bin.zip \
&& ln -s gradle-2.12 gradle

View File

@ -51,9 +51,7 @@
<g:if test="${r.published}">
<div>
<g:each in="${r.thumbnails}" var="thumbnail">
<g:each in="${thumbnail.content}" var="thumbnail_url">
<img src="${thumbnail_url}" class="thumbnail"/>
</g:each>
<img src="${thumbnail.content}" class="thumbnail"/>
</g:each>
</div>
</g:if>

View File

@ -65,6 +65,7 @@ module.exports = class CallbackEmitter extends EventEmitter {
const serverDomain = config.get("bbb.serverDomain");
const sharedSecret = config.get("bbb.sharedSecret");
const bearerAuth = config.get("bbb.auth2_0");
const timeout = config.get('hooks.requestTimeout');
// data to be sent
// note: keep keys in alphabetical order
@ -85,7 +86,8 @@ module.exports = class CallbackEmitter extends EventEmitter {
form: data,
auth: {
bearer: sharedSecret
}
},
timeout
};
}
else {
@ -103,7 +105,8 @@ module.exports = class CallbackEmitter extends EventEmitter {
maxRedirects: 10,
uri: callbackURL,
method: "POST",
form: data
form: data,
timeout
};
}

View File

@ -6,6 +6,9 @@ hooks:
permanentURLs:
__name: PERMANENT_HOOKS
__format: json
requestTimeout:
__name: REQUEST_TIMEOUT
__format: json
redis:
host: REDIS_HOST
port: REDIS_PORT

View File

@ -47,6 +47,8 @@ hooks:
- 60000
# Reset permanent interval when exceeding maximum attemps
permanentIntervalReset: 8
# Hook's request module timeout for socket conn establishment and/or responses (ms)
requestTimeout: 5000
# Mappings of internal to external meeting IDs
mappings:

View File

@ -924,9 +924,8 @@
}
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz"
},
"mkdirp": {
"version": "0.5.1",

View File

@ -1,2 +1 @@
BIGBLUEBUTTON_RELEASE=2.3.0-dev

View File

@ -65,6 +65,7 @@
# 2019-10-31 GTR Set IP and shared secret for bbb-webhooks
# 2019-11-09 GTR Keep HTML5 client logs permissions when cleaning logs
# 2020-05-20 NJH Add port 443 to --Network and clean up tmp file.
# 2020-06-23 JFS Remove defaultGuestPolicy warning for HTML5 client
#set -x
#set -e
@ -734,7 +735,7 @@ check_configuration() {
fi
fi
if [ "$IP" != "$NGINX_IP" ]; then
if [ "$IP" != "$NGINX_IP" ] && [ "_" != "$NGINX_IP" ]; then
if [ "$IP" != "$HOSTS" ]; then
echo "# IP does not match:"
echo "# IP from ifconfig: $IP"
@ -881,20 +882,6 @@ check_configuration() {
fi
fi
GUEST_POLICY=$(cat $BBB_WEB_CONFIG | grep -v '#' | sed -n '/^defaultGuestPolicy/{s/.*=//;p}')
if [ "$GUEST_POLICY" == "ASK_MODERATOR" ]; then
echo
echo "# Warning: defaultGuestPolicy is set to ASK_MODERATOR in"
echo "# $BBB_WEB_CONFIG"
echo "# This is not yet supported yet the HTML5 client."
echo "#"
echo "# To revert it to ALWAYS_ALLOW, see"
echo "#"
echo "# $SUDO sed -i s/^defaultGuestPolicy=.*$/defaultGuestPolicy=ALWAYS_ALLOW/g $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties"
echo "#"
echo
fi
if [ -f $HTML5_CONFIG ]; then
SVG_IMAGES_REQUIRED=$(cat $BBB_WEB_CONFIG | grep -v '#' | sed -n '/^svgImagesRequired/{s/.*=//;p}')
if [ "$SVG_IMAGES_REQUIRED" != "true" ]; then
@ -1122,7 +1109,8 @@ check_state() {
# Check FreeSWITCH
#
if ! echo "/quit" | /opt/freeswitch/bin/fs_cli - > /dev/null 2>&1; then
ESL_PASSWORD=$(xmlstarlet sel -t -m 'configuration/settings/param[@name="password"]' -v @value /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml)
if ! echo "/quit" | /opt/freeswitch/bin/fs_cli -p $ESL_PASSWORD - > /dev/null 2>&1; then
echo
echo "#"
echo "# Error: Unable to connect to the FreeSWITCH Event Socket Layer on port 8021"
@ -1234,7 +1222,7 @@ check_state() {
# Check if the local server can access the API. This is a common problem when setting up BigBlueButton behind
# a firewall
#
BBB_WEB=$(cat ${SERVLET_DIR}/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*\///;p}')
BBB_WEB=$(cat ${SERVLET_DIR}/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*\/\///;p}')
check_no_value server_name /etc/nginx/sites-available/bigbluebutton $BBB_WEB
COUNT=0
@ -1263,9 +1251,9 @@ check_state() {
# Check that BigBlueButton can connect to port 1935
#
if [ -f /usr/share/red5/red5-server.jar ]; then
if [[ ! -z $NGINX_IP && $DISTRIB_ID != "centos" ]]; then
if ! nc -w 3 $NGINX_IP 1935 > /dev/null; then
echo "# Error: Unable to connect to port 1935 (RTMP) on $NGINX_IP"
if [[ ! -z $RED5_IP && $DISTRIB_ID != "centos" ]]; then
if ! nc -w 3 $RED5_IP 1935 > /dev/null; then
echo "# Error: Unable to connect to port 1935 (RTMP) on $RED5_IP"
echo
fi
fi
@ -1545,7 +1533,7 @@ check_state() {
if [ "$FREESWITCH_SIP" != "$KURENTO_SIP" ]; then
echo
echo "#"
echo "# Kurento is will try to connect to $KURENTO_SIP but FreeSWITCH is listening on $FREESWITCH_SIP for port 5066"
echo "# Kurento will try to connect to $KURENTO_SIP but FreeSWITCH is listening on $FREESWITCH_SIP for port 5066"
echo "#"
echo "# To fix, run the commands"
echo "#"
@ -1690,6 +1678,7 @@ if [ $CHECK ]; then
echo " enableListenOnly: $(yq r $HTML5_CONFIG public.kurento.enableListenOnly)"
fi
if [ "$DISTRIB_CODENAME" == "xenial" ]; then
if ! java -version 2>&1 | grep -q "1.8.0"; then
echo
echo "# Warning: Did not detect Java 8 as default version"
@ -1698,6 +1687,7 @@ if [ $CHECK ]; then
echo " update-alternatives --config java"
echo " bbb-conf --restart"
fi
fi
check_state
echo
@ -2005,6 +1995,18 @@ if [ -n "$HOST" ]; then
#fi
fi
ESL_PASSWORD=$(xmlstarlet sel -t -m 'configuration/settings/param[@name="password"]' -v @value /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml)
if [ "$ESL_PASSWORD" == "ClueCon" ]; then
ESL_PASSWORD=$(openssl rand -hex 8)
echo "Changing default password for FreeSWITCH Event Socket Layer (see /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml)"
fi
# Update all references to ESL password
sudo sed -i "s/ClueCon/$ESL_PASSWORD/g" /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml
sudo sed -i "s/ClueCon/$ESL_PASSWORD/g" /usr/share/bbb-fsesl-akka/conf/application.conf
sudo yq w -i /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml freeswitch.esl_password "$ESL_PASSWORD"
echo "Restarting the BigBlueButton $BIGBLUEBUTTON_RELEASE ..."
stop_bigbluebutton
update_gstreamer

View File

@ -0,0 +1,35 @@
#!/bin/bash
#
# Restart Kurento every 24+ hours
#
if [ ! -f /var/tmp/bbb-kms-last-restart.txt ]; then
date +%Y-%m-%d\ %H:%M:%S > /var/tmp/bbb-kms-last-restart.txt
exit
fi
users=$(mongo --quiet mongodb://127.0.1.1:27017/meteor --eval "db.users.count({connectionStatus: 'online'})")
if [ "$users" -eq 0 ]; then
# Make sure 24 hours have passed since last restart
# Seconds since epoch for last restart
dt1=$(cat /var/tmp/bbb-kms-last-restart.txt)
t1=`date --date="$dt1" +%s`
# Current seconds since epoch
dt2=`date +%Y-%m-%d\ %H:%M:%S`
t2=`date --date="$dt2" +%s`
# Hours since last restart
let "tDiff=$t2-$t1"
let "hDiff=$tDiff/3600"
if [ "$hDiff" -ge 24 ]; then
systemctl restart kurento-media-server bbb-webrtc-sfu
date +%Y-%m-%d\ %H:%M:%S > /var/tmp/bbb-kms-last-restart.txt
fi
fi

View File

@ -1,3 +1,3 @@
#!/bin/bash
/opt/freeswitch/bin/fs_cli -x 'fsctl sync_clock_when_idle' > /var/log/freeswitch_sync_clock.log 2>&1
/opt/freeswitch/bin/fs_cli -x 'fsctl sync_clock_when_idle' -p $(xmlstarlet sel -t -m 'configuration/settings/param[@name="password"]' -v @value /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml) > /var/log/freeswitch_sync_clock.log 2>&1

Binary file not shown.

View File

@ -4,15 +4,15 @@
# but you can also edit it by hand.
meteor-base@1.4.0
mobile-experience@1.0.5
mongo@1.8.0
mobile-experience@1.1.0
mongo@1.10.0
reactive-var@1.0.11
standard-minifier-css@1.6.0
standard-minifier-js@2.6.0
es5-shim@4.8.0
ecmascript@0.14.0
shell-server@0.4.0
ecmascript@0.14.3
shell-server@0.5.0
static-html
react-meteor-data

View File

@ -1 +1 @@
METEOR@1.9
METEOR@1.10.2

View File

@ -1,12 +1,12 @@
allow-deny@1.1.0
autoupdate@1.6.0
babel-compiler@7.5.0
babel-compiler@7.5.3
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
blaze-tools@1.0.10
boilerplate-generator@1.6.0
caching-compiler@1.2.1
boilerplate-generator@1.7.0
caching-compiler@1.2.2
caching-html-compiler@1.1.3
callback-hook@1.3.0
cfs:micro-queue@0.0.6
@ -17,11 +17,11 @@ check@1.3.1
ddp@1.4.0
ddp-client@2.3.3
ddp-common@1.4.0
ddp-server@2.3.0
ddp-server@2.3.2
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.1
ecmascript@0.14.0
dynamic-import@0.5.2
ecmascript@0.14.3
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.10.0
ecmascript-runtime-server@0.9.0
@ -34,39 +34,39 @@ html-tools@1.0.11
htmljs@1.0.11
http@1.4.2
id-map@1.1.0
inter-process-messaging@0.1.0
launch-screen@1.1.1
inter-process-messaging@0.1.1
launch-screen@1.2.0
livedata@1.0.18
logging@1.1.20
meteor@1.9.3
meteor-base@1.4.0
minifier-css@1.5.0
minifier-css@1.5.1
minifier-js@2.6.0
minimongo@1.4.5
mobile-experience@1.0.5
mobile-status-bar@1.0.14
modern-browsers@0.1.4
minimongo@1.6.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.5
modules@0.15.0
modules-runtime@0.12.0
mongo@1.8.0
mongo@1.10.0
mongo-decimal@0.1.1
mongo-dev-server@1.1.0
mongo-id@1.0.7
nathantreid:css-modules@4.1.0
npm-mongo@3.3.0
npm-mongo@3.7.1
ordered-dict@1.1.0
promise@0.11.2
random@1.1.0
random@1.2.0
react-meteor-data@0.2.16
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.0
retry@1.1.0
rocketchat:streamer@1.0.1
rocketchat:streamer@1.1.0
routepolicy@1.1.0
session@1.2.0
shell-server@0.4.0
socket-stream-client@0.2.2
shell-server@0.5.0
socket-stream-client@0.3.0
spacebars-compiler@1.1.3
standard-minifier-css@1.6.0
standard-minifier-js@2.6.0
@ -75,6 +75,6 @@ templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.2
tracker@1.2.0
underscore@1.0.10
url@1.2.0
webapp@1.8.0
url@1.3.1
webapp@1.9.1
webapp-hashing@1.0.9

View File

@ -95,6 +95,7 @@ class SIPSession {
extraInfo: {
errorCode: error.code,
errorMessage: error.message,
callerIdName: this.user.callerIdName,
},
}, 'Full audio bridge failed to fetch STUN/TURN info');
return getFallbackStun();
@ -245,15 +246,15 @@ class SIPSession {
// translation
const isSafari = browser().name === 'safari';
logger.debug({ logCode: 'sip_js_creating_user_agent' }, 'Creating the user agent');
logger.debug({ logCode: 'sip_js_creating_user_agent', extraInfo: { callerIdName } }, 'Creating the user agent');
if (this.userAgent && this.userAgent.isConnected()) {
if (this.userAgent.configuration.hostPortParams === this.hostname) {
logger.debug({ logCode: 'sip_js_reusing_user_agent' }, 'Reusing the user agent');
logger.debug({ logCode: 'sip_js_reusing_user_agent', extraInfo: { callerIdName } }, 'Reusing the user agent');
resolve(this.userAgent);
return;
}
logger.debug({ logCode: 'sip_js_different_host_name' }, 'Different host name. need to kill');
logger.debug({ logCode: 'sip_js_different_host_name', extraInfo: { callerIdName } }, 'Different host name. need to kill');
}
const localSdpCallback = (sdp) => {
@ -407,7 +408,7 @@ class SIPSession {
let iceNegotiationTimeout;
const handleSessionAccepted = () => {
logger.info({ logCode: 'sip_js_session_accepted' }, 'Audio call session accepted');
logger.info({ logCode: 'sip_js_session_accepted', extraInfo: { callerIdName: this.user.callerIdName } }, 'Audio call session accepted');
clearTimeout(callTimeout);
currentSession.off('accepted', handleSessionAccepted);
@ -427,7 +428,7 @@ class SIPSession {
currentSession.on('accepted', handleSessionAccepted);
const handleSessionProgress = (update) => {
logger.info({ logCode: 'sip_js_session_progress' }, 'Audio call session progress update');
logger.info({ logCode: 'sip_js_session_progress', extraInfo: { callerIdName: this.user.callerIdName } }, 'Audio call session progress update');
clearTimeout(callTimeout);
currentSession.off('progress', handleSessionProgress);
};
@ -436,7 +437,10 @@ class SIPSession {
const handleConnectionCompleted = (peer) => {
logger.info({
logCode: 'sip_js_ice_connection_success',
extraInfo: { currentState: peer.iceConnectionState },
extraInfo: {
currentState: peer.iceConnectionState,
callerIdName: this.user.callerIdName,
},
}, `ICE connection success. Current state - ${peer.iceConnectionState}`);
clearTimeout(callTimeout);
clearTimeout(iceNegotiationTimeout);
@ -462,7 +466,7 @@ class SIPSession {
logger.error({
logCode: 'sip_js_call_terminated',
extraInfo: { cause },
extraInfo: { cause, callerIdName: this.user.callerIdName },
}, `Audio call terminated. cause=${cause}`);
let mappedCause;
@ -482,9 +486,9 @@ class SIPSession {
const handleIceNegotiationFailed = (peer) => {
if (iceCompleted) {
logger.error({ logCode: 'sipjs_ice_failed_after' }, 'ICE connection failed after success');
logger.error({ logCode: 'sipjs_ice_failed_after', extraInfo: { callerIdName: this.user.callerIdName } }, 'ICE connection failed after success');
} else {
logger.error({ logCode: 'sipjs_ice_failed_before' }, 'ICE connection failed before success');
logger.error({ logCode: 'sipjs_ice_failed_before', extraInfo: { callerIdName: this.user.callerIdName } }, 'ICE connection failed before success');
}
clearTimeout(callTimeout);
clearTimeout(iceNegotiationTimeout);
@ -500,7 +504,7 @@ class SIPSession {
const handleIceConnectionTerminated = (peer) => {
['iceConnectionClosed'].forEach(e => mediaHandler.off(e, handleIceConnectionTerminated));
if (!this.userRequestedHangup) {
logger.error({ logCode: 'sipjs_ice_closed' }, 'ICE connection closed');
logger.error({ logCode: 'sipjs_ice_closed', extraInfo: { callerIdName: this.user.callerIdName } }, 'ICE connection closed');
}
/*
this.callback({
@ -588,7 +592,7 @@ export default class SIPBridge extends BaseAudioBridge {
shouldTryReconnect = true;
} else if (hasFallbackDomain === true && hostname !== IPV4_FALLBACK_DOMAIN) {
message.silenceNotifications = true;
logger.info({ logCode: 'sip_js_attempt_ipv4_fallback' }, 'Attempting to fallback to IPv4 domain for audio');
logger.info({ logCode: 'sip_js_attempt_ipv4_fallback', extraInfo: { callerIdName: this.user.callerIdName } }, 'Attempting to fallback to IPv4 domain for audio');
hostname = IPV4_FALLBACK_DOMAIN;
shouldTryReconnect = true;
}
@ -704,7 +708,7 @@ export default class SIPBridge extends BaseAudioBridge {
} catch (err) {
logger.error({
logCode: 'audio_sip_changeoutputdevice_error',
extraInfo: { error: err },
extraInfo: { error: err, callerIdName: this.user.callerIdName },
}, 'Change Output Device error');
throw new Error(this.baseErrorCodes.MEDIA_ERROR);
}

View File

@ -6,7 +6,7 @@ import { extractCredentials } from '/imports/api/common/server/helpers';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
function breakouts() {
function breakouts(role) {
if (!this.userId) {
return Breakouts.find({ meetingId: '' });
}

View File

@ -0,0 +1,9 @@
import { Meteor } from 'meteor/meteor';
const ConnectionStatus = new Mongo.Collection('connection-status');
if (Meteor.isServer) {
ConnectionStatus._ensureIndex({ meetingId: 1, userId: 1 });
}
export default ConnectionStatus;

View File

@ -0,0 +1,2 @@
import './methods';
import './publishers';

View File

@ -0,0 +1,6 @@
import { Meteor } from 'meteor/meteor';
import addConnectionStatus from './methods/addConnectionStatus';
Meteor.methods({
addConnectionStatus,
});

View File

@ -0,0 +1,11 @@
import { check } from 'meteor/check';
import updateConnectionStatus from '/imports/api/connection-status/server/modifiers/updateConnectionStatus';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function addConnectionStatus(level) {
check(level, String);
const { meetingId, requesterUserId } = extractCredentials(this.userId);
updateConnectionStatus(meetingId, requesterUserId, level);
}

View File

@ -0,0 +1,14 @@
import ConnectionStatus from '/imports/api/connection-status';
import Logger from '/imports/startup/server/logger';
export default function clearConnectionStatus(meetingId) {
if (meetingId) {
return ConnectionStatus.remove({ meetingId }, () => {
Logger.info(`Cleared ConnectionStatus (${meetingId})`);
});
}
return ConnectionStatus.remove({}, () => {
Logger.info('Cleared ConnectionStatus (all)');
});
}

View File

@ -0,0 +1,37 @@
import ConnectionStatus from '/imports/api/connection-status';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
export default function updateConnectionStatus(meetingId, userId, level) {
check(meetingId, String);
check(userId, String);
const timestamp = new Date().getTime();
const selector = {
meetingId,
userId,
};
const modifier = {
meetingId,
userId,
level,
timestamp,
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Updating connection status: ${err}`);
}
const { insertedId } = numChanged;
if (insertedId) {
return Logger.info(`Added connection status userId=${userId} level=${level}`);
}
return Logger.verbose(`Update connection status userId=${userId} level=${level}`);
};
return ConnectionStatus.upsert(selector, modifier, cb);
}

View File

@ -0,0 +1,27 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import ConnectionStatus from '/imports/api/connection-status';
import { extractCredentials } from '/imports/api/common/server/helpers';
function connectionStatus() {
if (!this.userId) {
return ConnectionStatus.find({ meetingId: '' });
}
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
Logger.info(`Publishing connection status for ${meetingId} ${requesterUserId}`);
return ConnectionStatus.find({ meetingId });
}
function publish(...args) {
const boundNote = connectionStatus.bind(this);
return boundNote(...args);
}
Meteor.publish('connection-status', publish);

View File

@ -9,6 +9,7 @@ import createNote from '/imports/api/note/server/methods/createNote';
import createCaptions from '/imports/api/captions/server/methods/createCaptions';
import { addAnnotationsStreamer } from '/imports/api/annotations/server/streamer';
import { addCursorStreamer } from '/imports/api/cursor/server/streamer';
import BannedUsers from '/imports/api/users/server/store/bannedUsers';
export default function addMeeting(meeting) {
const meetingId = meeting.meetingProp.intId;
@ -145,6 +146,7 @@ export default function addMeeting(meeting) {
// better place we can run this post-creation routine?
createNote(meetingId);
createCaptions(meetingId);
BannedUsers.init(meetingId);
}
if (numChanged) {

View File

@ -15,11 +15,13 @@ import clearCaptions from '/imports/api/captions/server/modifiers/clearCaptions'
import clearPresentationPods from '/imports/api/presentation-pods/server/modifiers/clearPresentationPods';
import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoiceUsers';
import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo';
import clearConnectionStatus from '/imports/api/connection-status/server/modifiers/clearConnectionStatus';
import clearNote from '/imports/api/note/server/modifiers/clearNote';
import clearNetworkInformation from '/imports/api/network-information/server/modifiers/clearNetworkInformation';
import clearLocalSettings from '/imports/api/local-settings/server/modifiers/clearLocalSettings';
import clearRecordMeeting from './clearRecordMeeting';
import clearVoiceCallStates from '/imports/api/voice-call-states/server/modifiers/clearVoiceCallStates';
import clearVideoStreams from '/imports/api/video-streams/server/modifiers/clearVideoStreams';
export default function meetingHasEnded(meetingId) {
removeAnnotationsStreamer(meetingId);
@ -37,11 +39,13 @@ export default function meetingHasEnded(meetingId) {
clearUsersSettings(meetingId);
clearVoiceUsers(meetingId);
clearUserInfo(meetingId);
clearConnectionStatus(meetingId);
clearNote(meetingId);
clearNetworkInformation(meetingId);
clearLocalSettings(meetingId);
clearRecordMeeting(meetingId);
clearVoiceCallStates(meetingId);
clearVideoStreams(meetingId);
return Logger.info(`Cleared Meetings with id ${meetingId}`);
});

View File

@ -6,7 +6,7 @@ import { extractCredentials } from '/imports/api/common/server/helpers';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
function meetings() {
function meetings(role) {
if (!this.userId) {
return Meetings.find({ meetingId: '' });
}

View File

@ -46,6 +46,9 @@ const currentParameters = [
'bbb_enable_screen_sharing',
'bbb_enable_video',
'bbb_skip_video_preview',
'bbb_mirror_own_webcam',
// PRESENTATION
'bbb_force_restore_presentation_on_new_events',
// WHITEBOARD
'bbb_multi_user_pen_only',
'bbb_presenter_tools',
@ -57,6 +60,7 @@ const currentParameters = [
'bbb_auto_swap_layout',
'bbb_hide_presentation',
'bbb_show_participants_on_login',
'bbb_show_public_chat_on_login',
// OUTSIDE COMMANDS
'bbb_outside_toggle_self_voice',
'bbb_outside_toggle_recording',

View File

@ -16,7 +16,10 @@ const clearOtherSessions = (sessionUserId, current = false) => {
export default function handleValidateAuthToken({ body }, meetingId) {
const {
userId, valid, authToken, waitForApproval,
userId,
valid,
authToken,
waitForApproval,
} = body;
check(userId, String);

View File

@ -2,6 +2,8 @@ import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Users from '/imports/api/users';
import BannedUsers from '/imports/api/users/server/store/bannedUsers';
export default function removeUser(userId, banUser) {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -18,5 +20,9 @@ export default function removeUser(userId, banUser) {
banUser,
};
const removedUser = Users.findOne({ meetingId, userId }, { extId: 1 });
if (banUser && removedUser) BannedUsers.add(meetingId, removedUser.extId);
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, ejectedBy, payload);
}

View File

@ -2,12 +2,21 @@ import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import pendingAuthenticationsStore from '../store/pendingAuthentications';
import BannedUsers from '../store/bannedUsers';
export default function validateAuthToken(meetingId, requesterUserId, requesterToken) {
export default function validateAuthToken(meetingId, requesterUserId, requesterToken, externalId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ValidateAuthTokenReqMsg';
// Check if externalId is banned from the meeting
if (externalId) {
if (BannedUsers.has(meetingId, externalId)) {
Logger.warn(`A banned user with extId ${externalId} tried to enter in meeting ${meetingId}`);
return;
}
}
// Store reference of methodInvocationObject ( to postpone the connection userId definition )
pendingAuthenticationsStore.add(meetingId, requesterUserId, requesterToken, this);

View File

@ -51,7 +51,7 @@ function publishCurrentUser(...args) {
Meteor.publish('current-user', publishCurrentUser);
function users() {
function users(role) {
if (!this.userId) {
return Users.find({ meetingId: '' });
}

View File

@ -0,0 +1,35 @@
import Logger from '/imports/startup/server/logger';
class BannedUsers {
constructor() {
Logger.debug('BannedUsers :: Initializing');
this.store = {};
}
init(meetingId) {
Logger.debug('BannedUsers :: init', meetingId);
if (!this.store[meetingId]) this.store[meetingId] = new Set();
}
add(meetingId, externalId) {
Logger.debug('BannedUsers :: add', { meetingId, externalId });
if (!this.store[meetingId]) this.store[meetingId] = new Set();
this.store[meetingId].add(externalId);
}
delete(meetingId) {
Logger.debug('BannedUsers :: delete', meetingId);
delete this.store[meetingId];
}
has(meetingId, externalId) {
Logger.debug('BannedUsers :: has', { meetingId, externalId });
if (!this.store[meetingId]) this.store[meetingId] = new Set();
return this.store[meetingId].has(externalId);
}
}
export default new BannedUsers();

View File

@ -0,0 +1,14 @@
import Logger from '/imports/startup/server/logger';
import VideoStreams from '/imports/api/video-streams';
export default function clearVideoStreams(meetingId) {
if (meetingId) {
return VideoStreams.remove({ meetingId }, () => {
Logger.info(`Cleared VideoStreams in (${meetingId})`);
});
}
return VideoStreams.remove({}, () => {
Logger.info('Cleared VideoStreams in all meetings');
});
}

View File

@ -3,6 +3,8 @@ import { extractCredentials } from '/imports/api/common/server/helpers';
import RedisPubSub from '/imports/startup/server/redis';
import Users from '/imports/api/users';
import VoiceUsers from '/imports/api/voice-users';
import Meetings from '/imports/api/meetings';
import Logger from '/imports/startup/server/logger';
export default function muteToggle(uId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -27,6 +29,16 @@ export default function muteToggle(uId) {
const { listenOnly, muted } = voiceUser;
if (listenOnly) return;
// if allowModsToUnmuteUsers is false, users will be kicked out for attempting to unmute others
if (requesterUserId !== userToMute && muted) {
const meeting = Meetings.findOne({ meetingId },
{ fields: { 'usersProp.allowModsToUnmuteUsers': 1 } });
if (meeting.usersProp && !meeting.usersProp.allowModsToUnmuteUsers) {
Logger.warn(`Attempted unmuting by another user meetingId:${meetingId} requester: ${requesterUserId} userId: ${userToMute}`);
return;
}
}
const payload = {
userId: userToMute,
mutedBy: requesterUserId,

View File

@ -2,6 +2,7 @@ import VoiceUsers from '/imports/api/voice-users';
import { Meteor } from 'meteor/meteor';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
import ejectUserFromVoice from './methods/ejectUserFromVoice';
function voiceUser() {
if (!this.userId) {
@ -9,8 +10,23 @@ function voiceUser() {
}
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const onCloseConnection = Meteor.bindEnvironment(() => {
try {
// I used user because voiceUser is the function's name
const User = VoiceUsers.findOne({ meetingId, requesterUserId });
if (User) {
ejectUserFromVoice(requesterUserId);
}
} catch (e) {
Logger.error(`Exception while executing ejectUserFromVoice for ${requesterUserId}: ${e}`);
}
});
Logger.debug(`Publishing Voice User for ${meetingId} ${requesterUserId}`);
this._session.socket.on('close', _.debounce(onCloseConnection, 100));
return VoiceUsers.find({ meetingId });
}

View File

@ -7,7 +7,6 @@ import ErrorScreen from '/imports/ui/components/error-screen/component';
import MeetingEnded from '/imports/ui/components/meeting-ended/component';
import LoadingScreen from '/imports/ui/components/loading-screen/component';
import Settings from '/imports/ui/services/settings';
import AudioManager from '/imports/ui/services/audio-manager';
import logger from '/imports/startup/client/logger';
import Users from '/imports/api/users';
import { Session } from 'meteor/session';
@ -168,8 +167,8 @@ class Base extends Component {
const { updateLoadingState } = this;
const stateControls = { updateLoadingState };
const { loading } = this.state;
const codeError = Session.get('codeError');
const {
codeError,
ejected,
meetingExist,
meetingHasEnded,
@ -183,14 +182,15 @@ class Base extends Component {
}
if (ejected) {
AudioManager.exitAudio();
return (<MeetingEnded code="403" />);
}
if (meetingHasEnded && meetingIsBreakout) window.close();
if ((meetingHasEnded || User.loggedOut) && meetingIsBreakout) {
window.close();
return null;
}
if (((meetingHasEnded && !meetingIsBreakout)) || (codeError && (User && User.loggedOut))) {
AudioManager.exitAudio();
return (<MeetingEnded code={codeError} />);
}
@ -198,10 +198,11 @@ class Base extends Component {
// 680 is set for the codeError when the user requests a logout
if (codeError !== '680') {
logger.error({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${codeError}`);
}
return (<ErrorScreen code={codeError} />);
}
// this.props.annotationsHandler.stop();
return (<MeetingEnded code={codeError} />);
}
return (<AppContainer {...this.props} baseControls={stateControls} />);
}
@ -354,6 +355,18 @@ const BaseContainer = withTracker(() => {
});
}
if (getFromUserSettings('bbb_show_participants_on_login', true) && !deviceInfo.type().isPhone) {
Session.set('openPanel', 'userlist');
if (CHAT_ENABLED && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) {
Session.set('openPanel', 'chat');
Session.set('idChatOpen', PUBLIC_CHAT_ID);
}
} else {
Session.set('openPanel', '');
}
const codeError = Session.get('codeError');
return {
approved,
ejected,
@ -368,6 +381,7 @@ const BaseContainer = withTracker(() => {
meetingIsBreakout: AppService.meetingIsBreakout(),
subscriptionsReady: Session.get('subscriptionsReady'),
loggedIn,
codeError,
};
})(Base);

View File

@ -48,23 +48,36 @@ class ServerLoggerStream extends ServerStream {
class MeteorStream {
write(rec) {
const { fullInfo } = Auth;
const clientURL = window.location.href;
this.rec = rec;
if (fullInfo.meetingId != null) {
if (!this.rec.extraInfo) {
this.rec.extraInfo = {};
}
this.rec.extraInfo.clientURL = clientURL;
Meteor.call(
'logClient',
nameFromLevel[this.rec.level],
this.rec.msg,
this.rec.logCode,
this.rec.extraInfo || {},
this.rec.extraInfo,
fullInfo,
);
} else {
Meteor.call('logClient', nameFromLevel[this.rec.level], this.rec.msg);
Meteor.call(
'logClient',
nameFromLevel[this.rec.level],
this.rec.msg,
{ clientURL },
);
}
}
}
function createStreamForTarget(target, options) {
const TARGET_EXTERNAL = 'external';
const TARGET_CONSOLE = 'console';

View File

@ -14,6 +14,7 @@ import userLeaving from '/imports/api/users/server/methods/userLeaving';
const AVAILABLE_LOCALES = fs.readdirSync('assets/app/locales');
const FALLBACK_LOCALES = JSON.parse(Assets.getText('config/fallbackLocales.json'));
let avaibleLocalesNames = [];
Meteor.startup(() => {
const APP_CONFIG = Meteor.settings.public.app;
@ -172,7 +173,7 @@ WebApp.connectHandlers.use('/locales', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(JSON.stringify(locales));
res.end(JSON.stringify(avaibleLocalesNames));
});
WebApp.connectHandlers.use('/feedback', (req, res) => {

View File

@ -1,8 +1,9 @@
/* global __meteor_runtime_config__ */
import { Meteor } from 'meteor/meteor';
import fs from 'fs';
import YAML from 'yaml';
const YAML_FILE_PATH = 'assets/app/config/settings.yml';
const YAML_FILE_PATH = process.env.BBB_HTML5_SETTINGS || 'assets/app/config/settings.yml';
try {
if (fs.existsSync(YAML_FILE_PATH)) {
@ -14,5 +15,6 @@ try {
throw new Error('File doesn\'t exists');
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error on load configuration file.', error);
}

View File

@ -8,7 +8,6 @@ import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
import DropdownContent from '/imports/ui/components/dropdown/content/component';
import DropdownList from '/imports/ui/components/dropdown/list/component';
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
import PresentationUploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
import { withModalMounter } from '/imports/ui/components/modal/service';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
@ -78,6 +77,8 @@ const intlMessages = defineMessages({
},
});
const handlePresentationClick = () => Session.set('showUploadPresentationView', true);
class ActionsDropdown extends PureComponent {
constructor(props) {
super(props);
@ -86,7 +87,6 @@ class ActionsDropdown extends PureComponent {
this.pollId = _.uniqueId('action-item-');
this.takePresenterId = _.uniqueId('action-item-');
this.handlePresentationClick = this.handlePresentationClick.bind(this);
this.handleExternalVideoClick = this.handleExternalVideoClick.bind(this);
this.makePresentationItems = this.makePresentationItems.bind(this);
}
@ -161,7 +161,7 @@ class ActionsDropdown extends PureComponent {
label={formatMessage(presentationLabel)}
description={formatMessage(presentationDesc)}
key={this.presentationItemId}
onClick={this.handlePresentationClick}
onClick={handlePresentationClick}
/>
)
: null),
@ -196,13 +196,13 @@ class ActionsDropdown extends PureComponent {
const presentationItemElements = presentations.map((p) => {
const itemStyles = {};
itemStyles[styles.presentationItem] = true;
itemStyles[styles.isCurrent] = p.isCurrent;
itemStyles[styles.isCurrent] = p.current;
return (<DropdownListItem
className={cx(itemStyles)}
icon="file"
iconRight={p.isCurrent ? 'check' : null}
label={p.filename}
iconRight={p.current ? 'check' : null}
label={p.name}
description="uploaded presentation file"
key={`uploaded-presentation-${p.id}`}
onClick={() => {
@ -221,11 +221,6 @@ class ActionsDropdown extends PureComponent {
mountModal(<ExternalVideoModal />);
}
handlePresentationClick() {
const { mountModal } = this.props;
mountModal(<PresentationUploaderContainer />);
}
render() {
const {
intl,

View File

@ -0,0 +1,14 @@
import { withTracker } from 'meteor/react-meteor-data';
import Presentations from '/imports/api/presentations';
import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service';
import PresentationPodService from '/imports/ui/components/presentation-pod/service';
import ActionsDropdown from './component';
export default withTracker(() => {
const presentations = Presentations.find({ 'conversion.done': true }).fetch();
return ({
presentations,
setPresentation: PresentationUploaderService.setPresentation,
podIds: PresentationPodService.getPresentationPodIds(),
});
})(ActionsDropdown);

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import cx from 'classnames';
import { styles } from './styles.scss';
import DesktopShare from './desktop-share/component';
import ActionsDropdown from './actions-dropdown/component';
import ActionsDropdown from './actions-dropdown/container';
import AudioControlsContainer from '../audio/audio-controls/container';
import JoinVideoOptionsContainer from '../video-provider/video-button/container';
import CaptionsButtonContainer from '/imports/ui/components/actions-bar/captions/container';
@ -33,9 +33,6 @@ class ActionsBar extends PureComponent {
isPollingEnabled,
isThereCurrentPresentation,
allowExternalVideo,
presentations,
setPresentation,
podIds,
} = this.props;
const actionBarClasses = {};
@ -57,9 +54,6 @@ class ActionsBar extends PureComponent {
isSharingVideo,
stopExternalVideoShare,
isMeteorConnected,
presentations,
setPresentation,
podIds,
}}
/>
{isCaptionsAvailable

View File

@ -9,8 +9,6 @@ import Presentations from '/imports/api/presentations';
import ActionsBar from './component';
import Service from './service';
import ExternalVideoService from '/imports/ui/components/external-video-player/service';
import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service';
import PresentationPodService from '/imports/ui/components/presentation-pod/service';
import CaptionsService from '/imports/ui/components/captions/service';
import {
shareScreen,
@ -51,7 +49,4 @@ export default withTracker(() => ({
isThereCurrentPresentation: Presentations.findOne({ meetingId: Auth.meetingID, current: true },
{ fields: {} }),
allowExternalVideo: Meteor.settings.public.externalVideoPlayer.enabled,
presentations: PresentationUploaderService.getPresentations(),
setPresentation: PresentationUploaderService.setPresentation,
podIds: PresentationPodService.getPresentationPodIds(),
}))(injectIntl(ActionsBarContainer));

View File

@ -18,9 +18,11 @@ import ChatAlertContainer from '../chat/alert/container';
import BannerBarContainer from '/imports/ui/components/banner-bar/container';
import WaitingNotifierContainer from '/imports/ui/components/waiting-users/alert/container';
import LockNotifier from '/imports/ui/components/lock-viewers/notify/container';
import StatusNotifier from '/imports/ui/components/status-notifier/container';
import PingPongContainer from '/imports/ui/components/ping-pong/container';
import MediaService from '/imports/ui/components/media/service';
import ManyWebcamsNotifier from '/imports/ui/components/video-provider/many-users-notify/container';
import UploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
import { withDraggableContext } from '../media/webcam-draggable-overlay/context';
import { styles } from './styles';
@ -135,6 +137,8 @@ class App extends Component {
this.handleWindowResize();
window.addEventListener('resize', this.handleWindowResize, false);
window.ondragover = function (e) { e.preventDefault(); };
window.ondrop = function (e) { e.preventDefault(); };
if (ENABLE_NETWORK_MONITORING) {
if (navigator.connection) {
@ -338,6 +342,7 @@ class App extends Component {
{this.renderPanel()}
{this.renderSidebar()}
</section>
<UploaderContainer />
<BreakoutRoomInvitation />
<PollingContainer />
<ModalContainer />
@ -346,6 +351,7 @@ class App extends Component {
<ChatAlertContainer />
<WaitingNotifierContainer />
<LockNotifier />
<StatusNotifier status="raiseHand" />
<PingPongContainer />
<ManyWebcamsNotifier />
{customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null}

View File

@ -5,6 +5,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import getFromUserSettings from '/imports/ui/services/users-settings';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import MutedAlert from '/imports/ui/components/muted-alert/component';
import { styles } from './styles';
const intlMessages = defineMessages({
@ -63,6 +64,9 @@ class AudioControls extends PureComponent {
intl,
shortcuts,
isVoiceUser,
inputStream,
isViewer,
isPresenter,
} = this.props;
let joinIcon = 'audio_off';
@ -74,19 +78,17 @@ class AudioControls extends PureComponent {
}
}
return (
<span className={styles.container}>
{showMute && isVoiceUser
? (
const label = muted ? intl.formatMessage(intlMessages.unmuteAudio)
: intl.formatMessage(intlMessages.muteAudio);
const toggleMuteBtn = (
<Button
className={cx(styles.button, !talking || styles.glow, !muted || styles.btn)}
className={cx(styles.muteToggle, !talking || styles.glow, !muted || styles.btn)}
onClick={handleToggleMuteMicrophone}
disabled={disable}
hideLabel
label={muted ? intl.formatMessage(intlMessages.unmuteAudio)
: intl.formatMessage(intlMessages.muteAudio)}
aria-label={muted ? intl.formatMessage(intlMessages.unmuteAudio)
: intl.formatMessage(intlMessages.muteAudio)}
label={label}
aria-label={label}
color={!muted ? 'primary' : 'default'}
ghost={muted}
icon={muted ? 'mute' : 'unmute'}
@ -94,9 +96,14 @@ class AudioControls extends PureComponent {
circle
accessKey={shortcuts.togglemute}
/>
) : null}
);
return (
<span className={styles.container}>
{muted ? <MutedAlert {...{ inputStream, isViewer, isPresenter }} /> : null}
{showMute && isVoiceUser ? toggleMuteBtn : null}
<Button
className={cx(styles.button, inAudio || styles.btn)}
className={cx(inAudio || styles.btn)}
onClick={inAudio ? handleLeaveAudio : handleJoinAudio}
disabled={disable}
hideLabel
@ -111,7 +118,8 @@ class AudioControls extends PureComponent {
circle
accessKey={inAudio ? shortcuts.leaveaudio : shortcuts.joinaudio}
/>
</span>);
</span>
);
}
}

View File

@ -5,10 +5,14 @@ import AudioManager from '/imports/ui/services/audio-manager';
import { makeCall } from '/imports/ui/services/api';
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
import logger from '/imports/startup/client/logger';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import AudioControls from './component';
import AudioModalContainer from '../audio-modal/container';
import Service from '../service';
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
const AudioControlsContainer = props => <AudioControls {...props} />;
const processToggleMuteFromOutside = (e) => {
@ -54,7 +58,17 @@ const {
joinListenOnly,
} = Service;
export default lockContextContainer(withModalMounter(withTracker(({ mountModal, userLocks }) => ({
export default lockContextContainer(withModalMounter(withTracker(({ mountModal, userLocks }) => {
const currentUser = Users.findOne({ meetingId: Auth.meetingID, userId: Auth.userID }, {
fields: {
role: 1,
presenter: 1,
},
});
const isViewer = currentUser.role === ROLE_VIEWER;
const isPresenter = currentUser.presenter;
return ({
processToggleMuteFromOutside: arg => processToggleMuteFromOutside(arg),
showMute: isConnected() && !isListenOnly() && !isEchoTest() && !userLocks.userMic,
muted: isConnected() && !isListenOnly() && isMuted(),
@ -66,4 +80,8 @@ export default lockContextContainer(withModalMounter(withTracker(({ mountModal,
handleToggleMuteMicrophone: () => toggleMuteMicrophone(),
handleJoinAudio: () => (isConnected() ? joinListenOnly() : mountModal(<AudioModalContainer />)),
handleLeaveAudio,
}))(AudioControlsContainer)));
inputStream: AudioManager.inputStream,
isViewer,
isPresenter,
});
})(AudioControlsContainer)));

View File

@ -5,31 +5,39 @@
display: flex;
flex-flow: row;
> * {
margin: 0 var(--sm-padding-x);
.muteToggle {
margin-right: var(--sm-padding-x);
margin-left: 0;
@include mq($small-only) {
margin: 0 var(--sm-padding-y);
margin-right: var(--sm-padding-y);
}
}
> :first-child {
margin-left: 0;
margin-right: inherit;
[dir="rtl"] & {
margin-left: inherit;
margin-left: var(--sm-padding-x);
margin-right: 0;
@include mq($small-only) {
margin-left: var(--sm-padding-y);
}
}
}
> :last-child {
margin-left: inherit;
margin-left: var(--sm-padding-x);
margin-right: 0;
@include mq($small-only) {
margin-left: var(--sm-padding-y);
}
[dir="rtl"] & {
margin-left: 0;
margin-right: inherit;
margin-right: var(--sm-padding-x);
@include mq($small-only) {
margin-right: var(--sm-padding-y);
}
}
}
}

View File

@ -7,9 +7,9 @@ import LoadingScreen from '/imports/ui/components/loading-screen/component';
const STATUS_CONNECTING = 'connecting';
class AuthenticatedHandler extends Component {
static setError(codeError) {
Session.set('hasError', true);
if (codeError) Session.set('codeError', codeError);
static setError({ description, error }) {
if (error) Session.set('codeError', error);
Session.set('errorMessageDescription', description);
}
static shouldAuthenticate(status, lastStatus) {
@ -46,7 +46,7 @@ class AuthenticatedHandler extends Component {
extraInfo: { reason },
}, 'Encountered error while trying to authenticate');
AuthenticatedHandler.setError(reason.error);
AuthenticatedHandler.setError(reason);
callback();
};

View File

@ -83,7 +83,8 @@ export default injectNotify(injectIntl(withTracker(({
notify,
messageDuration,
timeEndedMessage,
alertMessageUnderOneMinute,
alertMessage,
alertUnderMinutes,
}) => {
const data = {};
if (breakoutRoom) {
@ -104,7 +105,9 @@ export default injectNotify(injectIntl(withTracker(({
if (timeRemaining >= 0 && timeRemainingInterval) {
if (timeRemaining > 0) {
const time = getTimeRemaining();
if (time === (1 * 60) && alertMessageUnderOneMinute) notify(alertMessageUnderOneMinute, 'info', 'rooms');
if (time === (alertUnderMinutes * 60) && alertMessage) {
notify(alertMessage, 'info', 'rooms');
}
data.message = intl.formatMessage(messageDuration, { 0: humanizeSeconds(time) });
} else {
clearInterval(timeRemainingInterval);

View File

@ -6,6 +6,7 @@ import { Session } from 'meteor/session';
import logger from '/imports/startup/client/logger';
import { styles } from './styles';
import BreakoutRoomContainer from './breakout-remaining-time/container';
import VideoService from '/imports/ui/components/video-provider/service';
const intlMessages = defineMessages({
breakoutTitle: {
@ -208,6 +209,7 @@ class BreakoutRoom extends PureComponent {
logCode: 'breakoutroom_join',
extraInfo: { logType: 'user_action' },
}, 'joining breakout room closed audio in the main room');
VideoService.exitVideo();
}
}
disabled={disable}

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Tooltip from '/imports/ui/components/tooltip/component';
import TooltipContainer from '/imports/ui/components/tooltip/container';
import { styles } from './styles';
import Icon from '../icon/component';
import BaseButton from './base/component';
@ -109,6 +109,21 @@ export default class Button extends BaseButton {
return propClassNames;
}
_cleanProps(otherProps) {
const remainingProps = Object.assign({}, otherProps);
delete remainingProps.icon;
delete remainingProps.customIcon;
delete remainingProps.size;
delete remainingProps.color;
delete remainingProps.ghost;
delete remainingProps.circle;
delete remainingProps.block;
delete remainingProps.hideLabel;
delete remainingProps.tooltipLabel;
return remainingProps;
}
render() {
const {
circle,
@ -124,11 +139,11 @@ export default class Button extends BaseButton {
if ((hideLabel && !ariaExpanded) || tooltipLabel) {
const buttonLabel = label || ariaLabel;
return (
<Tooltip
<TooltipContainer
title={tooltipLabel || buttonLabel}
>
{this[renderFuncName]()}
</Tooltip>
</TooltipContainer>
);
}
@ -142,16 +157,7 @@ export default class Button extends BaseButton {
...otherProps
} = this.props;
const remainingProps = Object.assign({}, otherProps);
delete remainingProps.icon;
delete remainingProps.customIcon;
delete remainingProps.size;
delete remainingProps.color;
delete remainingProps.ghost;
delete remainingProps.circle;
delete remainingProps.block;
delete remainingProps.hideLabel;
delete remainingProps.tooltipLabel;
const remainingProps = this._cleanProps(otherProps);
/* TODO: We can change this and make the button with flexbox to avoid html
changes */
@ -177,14 +183,7 @@ export default class Button extends BaseButton {
...otherProps
} = this.props;
const remainingProps = Object.assign({}, otherProps);
delete remainingProps.icon;
delete remainingProps.color;
delete remainingProps.ghost;
delete remainingProps.circle;
delete remainingProps.block;
delete remainingProps.hideLabel;
delete remainingProps.tooltipLabel;
const remainingProps = this._cleanProps(otherProps);
return (
<BaseButton

View File

@ -28,7 +28,6 @@ class Captions extends React.Component {
} = this.props;
if (padId === nextProps.padId) {
if (this.text !== '') this.ariaText = this.text;
if (revs === nextProps.revs && !nextState.clear) return false;
}
return true;
@ -56,7 +55,9 @@ class Captions extends React.Component {
const { clear } = this.state;
if (clear) {
this.text = '';
this.ariaText = '';
} else {
this.ariaText = CaptionsService.formatCaptionsText(data);
const text = this.text + data;
this.text = CaptionsService.formatCaptionsText(text);
}
@ -105,15 +106,13 @@ class Captions extends React.Component {
return (
<div>
<div
aria-hidden
style={captionStyles}
>
<div style={captionStyles}>
{this.text}
</div>
<div
style={visuallyHidden}
aria-live={this.text === '' && this.ariaText !== '' ? 'polite' : 'off'}
aria-atomic
aria-live="polite"
>
{this.ariaText}
</div>

View File

@ -157,11 +157,11 @@ export default injectIntl(withTracker(({ intl }) => {
return {
...message,
content: message.content.map(content => ({
content: message.content ? message.content.map(content => ({
...content,
text: content.text in intlMessages
? `<b><i>${intl.formatMessage(intlMessages[content.text], systemMessageIntl)}</i></b>` : content.text,
})),
})) : [],
};
});

View File

@ -2,6 +2,10 @@
@import "/imports/ui/stylesheets/mixins/_indicators";
@import "/imports/ui/stylesheets/variables/_all";
:root {
--max-chat-input-msg-height: .93rem;
}
.form {
flex-grow: 0;
flex-shrink: 0;
@ -100,28 +104,20 @@
}
}
.info {
&:before {
content: "\00a0"; // non-breaking space
}
}
.error,
.info {
font-size: calc(var(--font-size-base) * .75);
color: var(--color-gray-dark);
text-align: left;
padding: var(--border-size) 0;
[dir="rtl"] & {
text-align: right;
}
position: relative;
}
.spacer {
&:before {
content: "\00a0"; // non-breaking space
}
.error,
.info,
.space {
height: var(--max-chat-input-msg-height);
max-height: var(--max-chat-input-msg-height);
}
.coupleTyper,
@ -146,11 +142,15 @@
flex-direction: row;
> span {
display: flex;
flex-direction: row;
display: block;
width: 100%;
line-height: var(--font-size-md);
}
text-align: left;
[dir="rtl"] & {
text-align: right;
}
}
.error {

View File

@ -97,16 +97,16 @@ class TypingIndicator extends PureComponent {
indicatorEnabled,
} = this.props;
const typingElement = this.renderTypingElement();
const typingElement = indicatorEnabled ? this.renderTypingElement() : null;
const showSpacer = (indicatorEnabled ? !typingElement : !error);
const style = {};
style[styles.error] = !!error;
style[styles.info] = !error;
style[styles.spacer] = !!typingElement;
return (
<div className={cx(styles.info, (showSpacer && styles.spacer))}>
<div className={styles.typingIndicator}>{typingElement}</div>
{error
&& <div className={cx(styles.typingIndicator, styles.error)}>{error}</div>
}
<div className={cx(style)}>
<span className={styles.typingIndicator}>{error || typingElement}</span>
</div>
);
}

View File

@ -16,7 +16,7 @@ const propTypes = {
name: PropTypes.string,
}),
messages: PropTypes.arrayOf(Object).isRequired,
time: PropTypes.number.isRequired,
time: PropTypes.number,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
@ -30,6 +30,7 @@ const defaultProps = {
user: null,
scrollArea: null,
lastReadMessageTime: 0,
time: 0,
};
const intlMessages = defineMessages({

View File

@ -31,9 +31,16 @@
justify-content: space-around;
overflow: hidden;
height: 100vh;
:global(.browser-chrome) & {
transform: translateZ(0);
}
@include mq($small-only) {
transform: none !important;
}
}
.header {
position: relative;
top: var(--poll-header-offset);

View File

@ -0,0 +1,157 @@
import React, { PureComponent } from 'react';
import { FormattedTime, defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import cx from 'classnames';
import UserAvatar from '/imports/ui/components/user-avatar/component';
import SlowConnection from '/imports/ui/components/slow-connection/component';
import Modal from '/imports/ui/components/modal/simple/component';
import { styles } from './styles';
const STATS = Meteor.settings.public.stats;
const intlMessages = defineMessages({
ariaTitle: {
id: 'app.connection-status.ariaTitle',
description: 'Connection status aria title',
},
title: {
id: 'app.connection-status.title',
description: 'Connection status title',
},
description: {
id: 'app.connection-status.description',
description: 'Connection status description',
},
empty: {
id: 'app.connection-status.empty',
description: 'Connection status empty',
},
more: {
id: 'app.connection-status.more',
description: 'More about conectivity issues',
},
offline: {
id: 'app.connection-status.offline',
description: 'Offline user',
},
});
const propTypes = {
closeModal: PropTypes.func.isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
};
class ConnectionStatusComponent extends PureComponent {
renderEmpty() {
const { intl } = this.props;
return (
<div className={styles.item}>
<div className={styles.left}>
<div className={styles.name}>
<div className={styles.text}>
{intl.formatMessage(intlMessages.empty)}
</div>
</div>
</div>
</div>
);
}
renderConnections() {
const {
connectionStatus,
intl,
} = this.props;
if (connectionStatus.length === 0) return this.renderEmpty();
return connectionStatus.map((conn, index) => {
const dateTime = new Date(conn.timestamp);
const itemStyle = {};
itemStyle[styles.even] = index % 2 === 0;
const textStyle = {};
textStyle[styles.offline] = conn.offline;
return (
<div
key={index}
className={cx(styles.item, itemStyle)}
>
<div className={styles.left}>
<div className={styles.avatar}>
<UserAvatar
className={styles.icon}
you={conn.you}
moderator={conn.moderator}
color={conn.color}
>
{conn.name.toLowerCase().slice(0, 2)}
</UserAvatar>
</div>
<div className={styles.name}>
<div className={cx(styles.text, textStyle)}>
{conn.name}
{conn.offline ? ` (${intl.formatMessage(intlMessages.offline)})` : null}
</div>
</div>
<div className={styles.status}>
<SlowConnection effectiveConnectionType={conn.level} />
</div>
</div>
<div className={styles.right}>
<div className={styles.time}>
<time dateTime={dateTime}>
<FormattedTime value={dateTime} />
</time>
</div>
</div>
</div>
);
});
}
render() {
const {
closeModal,
intl,
} = this.props;
return (
<Modal
overlayClassName={styles.overlay}
className={styles.modal}
onRequestClose={closeModal}
hideBorder
contentLabel={intl.formatMessage(intlMessages.ariaTitle)}
>
<div className={styles.container}>
<div className={styles.header}>
<h2 className={styles.title}>
{intl.formatMessage(intlMessages.title)}
</h2>
</div>
<div className={styles.description}>
{intl.formatMessage(intlMessages.description)}{' '}
<a href={STATS.help} target="_blank" rel="noopener noreferrer">
{`(${intl.formatMessage(intlMessages.more)})`}
</a>
</div>
<div className={styles.content}>
<div className={styles.wrapper}>
{this.renderConnections()}
</div>
</div>
</div>
</Modal>
);
}
}
ConnectionStatusComponent.propTypes = propTypes;
export default injectIntl(ConnectionStatusComponent);

View File

@ -0,0 +1,12 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/modal/service';
import ConnectionStatusService from '../service';
import ConnectionStatusComponent from './component';
const connectionStatusContainer = props => <ConnectionStatusComponent {...props} />;
export default withModalMounter(withTracker(({ mountModal }) => ({
closeModal: () => mountModal(null),
connectionStatus: ConnectionStatusService.getConnectionStatus(),
}))(connectionStatusContainer));

View File

@ -0,0 +1,137 @@
@import '/imports/ui/stylesheets/mixins/focus';
@import '/imports/ui/stylesheets/variables/_all';
@import "/imports/ui/components/modal/simple/styles";
:root {
--modal-margin: 3rem;
--title-position-left: 2.2rem;
--closeBtn-position-left: 2.5rem;
}
.title {
left: var(--title-position-left);
right: auto;
color: var(--color-gray-dark);
font-weight: bold;
font-size: var(--font-size-large);
text-align: center;
[dir="rtl"] & {
left: auto;
right: var(--title-position-left);
}
}
.container {
margin: 0 var(--modal-margin) var(--lg-padding-x);
}
.modal {
@extend .modal;
padding: var(--jumbo-padding-y);
}
.overlay {
@extend .overlay;
}
.description {
text-align: center;
color: var(--color-gray);
margin-bottom: var(--jumbo-padding-y)
}
.label {
color: var(--color-gray-label);
font-size: var(--font-size-small);
margin-bottom: var(--lg-padding-y);
}
.header {
margin: 0;
padding: 0;
border: none;
line-height: var(--title-position-left);
margin-bottom: var(--lg-padding-y);
}
.content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0;
}
.wrapper {
display: block;
width: 100%;
max-height: 24rem;
}
.item {
display: flex;
width: 100%;
height: 4rem;
}
.even {
background-color: var(--color-off-white);
}
.left {
display: flex;
width: 100%;
height: 100%;
.avatar {
display: flex;
width: 4rem;
height: 100%;
justify-content: center;
align-items: center;
.icon {
min-width: 2.25rem;
height: 2.25rem;
}
}
.name {
display: grid;
width: 100%;
height: 100%;
align-items: center;
.text {
padding-left: .5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.offline {
font-style: italic;
}
}
.status {
display: flex;
width: 6rem;
height: 100%;
justify-content: center;
align-items: center;
}
}
.right {
display: flex;
width: 5rem;
height: 100%;
.time {
display: flex;
align-items: center;
width: 100%;
height: 100%;
}
}

View File

@ -0,0 +1,134 @@
import ConnectionStatus from '/imports/api/connection-status';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import { makeCall } from '/imports/ui/services/api';
const STATS = Meteor.settings.public.stats;
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
let audioStats = '';
const audioStatsDep = new Tracker.Dependency();
let statsTimeout = null;
const getHelp = () => STATS.help;
const getLevel = () => STATS.level;
const getAudioStats = () => {
audioStatsDep.depend();
return audioStats;
};
const setAudioStats = (level = '') => {
if (audioStats !== level) {
audioStats = level;
audioStatsDep.changed();
addConnectionStatus(level);
}
};
const handleAudioStatsEvent = (event) => {
const { detail } = event;
if (detail) {
const { loss, jitter } = detail;
let active = false;
// From higher to lower
for (let i = STATS.level.length - 1; i >= 0; i--) {
if (loss > STATS.loss[i] || jitter > STATS.jitter[i]) {
active = true;
setAudioStats(STATS.level[i]);
break;
}
}
if (active) {
if (statsTimeout !== null) clearTimeout(statsTimeout);
statsTimeout = setTimeout(() => {
setAudioStats();
}, STATS.length * STATS.interval);
}
}
};
const addConnectionStatus = (level) => {
if (level !== '') makeCall('addConnectionStatus', level);
};
const sortLevel = (a, b) => {
const indexOfA = STATS.level.indexOf(a.level);
const indexOfB = STATS.level.indexOf(b.level);
if (indexOfA < indexOfB) return 1;
if (indexOfA === indexOfB) return 0;
if (indexOfA > indexOfB) return -1;
};
const getConnectionStatus = () => {
const connectionStatus = ConnectionStatus.find(
{ meetingId: Auth.meetingID },
).fetch().map(status => {
const {
userId,
level,
timestamp,
} = status;
return {
userId,
level,
timestamp,
};
});
return Users.find(
{ meetingId: Auth.meetingID },
{ fields:
{
userId: 1,
name: 1,
role: 1,
color: 1,
connectionStatus: 1,
},
},
).fetch().reduce((result, user) => {
const {
userId,
name,
role,
color,
connectionStatus: userStatus,
} = user;
const status = connectionStatus.find(status => status.userId === userId);
if (status) {
result.push({
name,
offline: userStatus === 'offline',
you: Auth.userID === userId,
moderator: role === ROLE_MODERATOR,
color,
level: status.level,
timestamp: status.timestamp,
});
}
return result;
}, []).sort(sortLevel);
};
const isEnabled = () => STATS.enabled;
if (STATS.enabled) {
window.addEventListener('audiostats', handleAudioStatsEvent);
}
export default {
addConnectionStatus,
getConnectionStatus,
getAudioStats,
getHelp,
getLevel,
isEnabled,
};

View File

@ -1,10 +1,9 @@
import React from 'react';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { Meteor } from 'meteor/meteor';
import Button from '/imports/ui/components/button/component';
import logoutRouteHandler from '/imports/utils/logoutRouteHandler';
import { Session } from 'meteor/session';
import AudioManager from '/imports/ui/services/audio-manager';
import { styles } from './styles';
const intlMessages = defineMessages({
@ -28,10 +27,6 @@ const intlMessages = defineMessages({
400: {
id: 'app.error.400',
},
leave: {
id: 'app.error.leaveLabel',
description: 'aria-label for leaving',
},
});
const propTypes = {
@ -45,8 +40,9 @@ const defaultProps = {
code: 500,
};
class ErrorScreen extends React.PureComponent {
class ErrorScreen extends PureComponent {
componentDidMount() {
AudioManager.exitAudio();
Meteor.disconnect();
}
@ -65,30 +61,21 @@ class ErrorScreen extends React.PureComponent {
return (
<div className={styles.background}>
<h1 className={styles.codeError}>
{code}
</h1>
<h1 className={styles.message}>
{formatedMessage}
</h1>
<div className={styles.separator} />
<div>
{children}
</div>
{
!Session.get('errorMessageDescription') || (
<div className={styles.sessionMessage}>
{Session.get('errorMessageDescription')}
</div>)
}
<div className={styles.separator} />
<h1 className={styles.codeError}>
{code}
</h1>
<div>
<Button
size="sm"
color="primary"
className={styles.button}
onClick={logoutRouteHandler}
label={intl.formatMessage(intlMessages.leave)}
/>
{children}
</div>
</div>
);

View File

@ -20,20 +20,21 @@
.message {
margin: 0;
color: var(--color-gray-light);
font-size: 1.25rem;
color: var(--color-white);
font-size: 1.75rem;
font-weight: 400;
margin-bottom: 1rem;
}
.sessionMessage {
@extend .message;
font-size: var(--font-size-small);
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--color-white);
}
.codeError {
margin: 0;
font-size: 5rem;
font-size: 1.5rem;
color: var(--color-white);
}
@ -50,4 +51,3 @@
min-width: 9rem;
height: 2rem;
}

View File

@ -47,6 +47,12 @@ class VideoPlayer extends Component {
};
this.opts = {
// default option for all players, can be overwritten
playerOptions: {
autoplay: true,
playsinline: true,
controls: true,
},
file: {
attributes: {
controls: true,
@ -67,6 +73,12 @@ class VideoPlayer extends Component {
controls: isPresenter ? 1 : 2,
},
},
twitch: {
options: {
controls: true,
},
playerId: 'externalVideoPlayerTwitch',
},
preload: true,
};
@ -212,6 +224,11 @@ class VideoPlayer extends Component {
setPlaybackRate(rate) {
const intPlayer = this.player && this.player.getInternalPlayer();
const currentRate = this.getCurrentPlaybackRate();
if (currentRate === rate) {
return;
}
this.setState({ playbackRate: rate });
if (intPlayer && intPlayer.setPlaybackRate) {

View File

@ -62,6 +62,10 @@ export class ArcPlayer extends Component {
this.onStateChange = this.onStateChange.bind(this);
}
componentDidMount () {
this.props.onMount && this.props.onMount(this)
}
load() {
new Promise((resolve, reject) => {
this.render();

View File

@ -14,7 +14,6 @@ const propTypes = {
class JoinHandler extends Component {
static setError(codeError) {
Session.set('hasError', true);
if (codeError) Session.set('codeError', codeError);
}
@ -180,7 +179,7 @@ class JoinHandler extends Component {
logUserInfo();
Tracker.autorun(async (cd) => {
const user = Users.findOne({ userId: Auth.userID, authed: true }, { fields: { _id: 1 } });
const user = Users.findOne({ userId: Auth.userID, approved: true }, { fields: { _id: 1 } });
if (user) {
await setCustomData(response);

View File

@ -107,9 +107,10 @@ export default withDraggableConsumer(withModalMounter(withTracker(() => {
const { dataSaving } = Settings;
const { viewParticipantsWebcams, viewScreenshare } = dataSaving;
const hidePresentation = getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation);
const autoSwapLayout = getFromUserSettings('userdata-bbb_auto_swap_layout', LAYOUT_CONFIG.autoSwapLayout);
const { current_presentation: hasPresentation } = MediaService.getPresentationInfo();
const data = {
children: <DefaultContent />,
children: <DefaultContent {...{ autoSwapLayout, hidePresentation }} />,
audioModalIsOpen: Session.get('audioModalIsOpen'),
};

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { Meteor } from 'meteor/meteor';
@ -10,6 +10,7 @@ import Rating from './rating/component';
import { styles } from './styles';
import logger from '/imports/startup/client/logger';
import Users from '/imports/api/users';
import AudioManager from '/imports/ui/services/audio-manager';
const intlMessage = defineMessages({
410: {
@ -89,7 +90,7 @@ const propTypes = {
code: PropTypes.string.isRequired,
};
class MeetingEnded extends React.PureComponent {
class MeetingEnded extends PureComponent {
static getComment() {
const textarea = document.getElementById('feedbackComment');
const comment = textarea.value;
@ -110,9 +111,8 @@ class MeetingEnded extends React.PureComponent {
this.setSelectedStar = this.setSelectedStar.bind(this);
this.sendFeedback = this.sendFeedback.bind(this);
this.shouldShowFeedback = getFromUserSettings('bbb_ask_for_feedback_on_logout', Meteor.settings.public.app.askForFeedbackOnLogout);
}
componentDidMount() {
AudioManager.exitAudio();
Meteor.disconnect();
}
@ -179,9 +179,9 @@ class MeetingEnded extends React.PureComponent {
return (
<div className={styles.parent}>
<div className={styles.modal}>
<div className={styles.modal} data-test="meetingEndedModal">
<div className={styles.content}>
<h1 className={styles.title} data-test="meetingEndedModalTitle">
<h1 className={styles.title}>
{
intl.formatMessage(intlMessage[code] || intlMessage[430])
}
@ -192,7 +192,7 @@ class MeetingEnded extends React.PureComponent {
: intl.formatMessage(intlMessage.messageEnded)}
</div>
{this.shouldShowFeedback ? (
<div>
<div data-test="rating">
<Rating
total="5"
onRate={this.setSelectedStar}

View File

@ -0,0 +1,84 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import hark from 'hark';
import Icon from '/imports/ui/components/icon/component';
import cx from 'classnames';
import { styles } from './styles';
const MUTE_ALERT_CONFIG = Meteor.settings.public.app.mutedAlert;
const propTypes = {
inputStream: PropTypes.object.isRequired,
};
class MutedAlert extends Component {
constructor(props) {
super(props);
this.state = {
visible: false,
};
this.speechEvents = null;
this.timer = null;
this.resetTimer = this.resetTimer.bind(this);
}
componentDidMount() {
this._isMounted = true;
const { inputStream } = this.props;
const { interval, threshold, duration } = MUTE_ALERT_CONFIG;
this.speechEvents = hark(inputStream, { interval, threshold });
this.speechEvents.on('speaking', () => {
this.resetTimer();
if (this._isMounted) this.setState({ visible: true });
});
this.speechEvents.on('stopped_speaking', () => {
if (this._isMounted) {
this.timer = setTimeout(() => this.setState(
{ visible: false },
), duration);
}
});
}
componentWillUnmount() {
this._isMounted = false;
if (this.speechEvents) this.speechEvents.stop();
this.resetTimer();
}
resetTimer() {
if (this.timer) clearTimeout(this.timer);
this.timer = null;
}
render() {
const { enabled } = MUTE_ALERT_CONFIG;
if (!enabled) return null;
const { isViewer, isPresenter } = this.props;
const { visible } = this.state;
const style = {};
style[styles.alignForMod] = !isViewer || isPresenter;
return visible ? (
<div className={cx(styles.muteWarning, style)}>
<span>
<FormattedMessage
id="app.muteWarning.label"
description="Warning when someone speaks while muted"
values={{
0: <Icon iconName="mute" />,
}}
/>
</span>
</div>
) : null;
}
}
MutedAlert.propTypes = propTypes;
export default MutedAlert;

View File

@ -0,0 +1,28 @@
@import "../../stylesheets/variables/_all";
.muteWarning {
position: absolute;
color: var(--color-white);
background-color: var(--color-tip-bg);
text-align: center;
line-height: 1;
font-size: var(--font-size-xl);
padding: var(--md-padding-x);
border-radius: var(--border-radius);
top: -50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 5;
> span {
white-space: nowrap;
}
@include mq($small-only) {
font-size: var(--font-size-md);;
}
}
.alignForMod {
left: 52.25%;
}

View File

@ -212,6 +212,7 @@ class SettingsDropdown extends PureComponent {
const logoutOption = (
<DropdownListItem
key="list-item-logout"
data-test="logout"
icon="logout"
label={intl.formatMessage(intlMessages.leaveSessionLabel)}
description={intl.formatMessage(intlMessages.leaveSessionDesc)}

View File

@ -2,6 +2,7 @@ import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import VoiceUsers from '/imports/api/voice-users';
import Auth from '/imports/ui/services/auth';
import { debounce } from 'lodash';
import TalkingIndicator from './component';
import { makeCall } from '/imports/ui/services/api';
import Service from './service';
@ -57,7 +58,7 @@ export default withTracker(() => {
return {
talkers,
muteUser,
muteUser: id => debounce(muteUser(id), 500, { leading: true, trailing: false }),
openPanel: Session.get('openPanel'),
};
})(TalkingIndicatorContainer);

View File

@ -64,9 +64,10 @@ class Note extends Component {
onClick={() => {
Session.set('openPanel', 'userlist');
}}
data-test="hideNoteLabel"
aria-label={intl.formatMessage(intlMessages.hideNoteLabel)}
label={intl.formatMessage(intlMessages.title)}
icon={isRTL ? "right_arrow" : "left_arrow"}
icon={isRTL ? 'right_arrow' : 'left_arrow'}
className={styles.hideBtn}
/>
</div>

View File

@ -8,6 +8,7 @@ import Meetings, { MeetingTimeRemaining } from '/imports/api/meetings';
import Users from '/imports/api/users';
import BreakoutRemainingTime from '/imports/ui/components/breakout-room/breakout-remaining-time/container';
import SlowConnection from '/imports/ui/components/slow-connection/component';
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
import { styles } from './styles.scss';
import breakoutService from '/imports/ui/components/breakout-room/service';
@ -29,6 +30,9 @@ const ENABLE_NETWORK_MONITORING = Meteor.settings.public.networkMonitoring.enabl
const HELP_LINK = METEOR_SETTINGS_APP.helpLink;
const REMAINING_TIME_THRESHOLD = METEOR_SETTINGS_APP.remainingTimeThreshold;
const REMAINING_TIME_ALERT_THRESHOLD = METEOR_SETTINGS_APP.remainingTimeAlertThreshold;
const intlMessages = defineMessages({
failedMessage: {
id: 'app.failedMessage',
@ -66,13 +70,13 @@ const intlMessages = defineMessages({
id: 'app.meeting.meetingTimeHasEnded',
description: 'Message that tells time has ended and meeting will close',
},
alertMeetingEndsUnderOneMinute: {
id: 'app.meeting.alertMeetingEndsUnderOneMinute',
description: 'Alert that tells that the meeting end under a minute',
alertMeetingEndsUnderMinutes: {
id: 'app.meeting.alertMeetingEndsUnderMinutes',
description: 'Alert that tells that the meeting ends under x minutes',
},
alertBreakoutEndsUnderOneMinute: {
id: 'app.meeting.alertBreakoutEndsUnderOneMinute',
description: 'Alert that tells that the breakout end under a minute',
alertBreakoutEndsUnderMinutes: {
id: 'app.meeting.alertBreakoutEndsUnderMinutes',
description: 'Alert that tells that the breakout ends under x minutes',
},
slowEffectiveConnectionDetected: {
id: 'app.network.connection.effective.slow',
@ -146,6 +150,22 @@ export default injectIntl(withTracker(({ intl }) => {
}
}
if (ConnectionStatusService.isEnabled()) {
const stats = ConnectionStatusService.getAudioStats();
if (stats) {
if (ConnectionStatusService.getLevel().includes(stats)) {
data.message = (
<SlowConnection effectiveConnectionType={stats}>
{intl.formatMessage(intlMessages.slowEffectiveConnectionDetected)}{' '}
<a href={ConnectionStatusService.getHelp()} target="_blank" rel="noopener noreferrer">
{intl.formatMessage(intlMessages.slowEffectiveConnectionHelpLink)}
</a>
</SlowConnection>
);
}
}
}
if (!connected) {
data.color = 'primary';
switch (status) {
@ -181,6 +201,8 @@ export default injectIntl(withTracker(({ intl }) => {
const meetingId = Auth.meetingID;
const breakouts = breakoutService.getBreakouts();
const msg = { id: `${intlMessages.alertBreakoutEndsUnderMinutes.id}${REMAINING_TIME_ALERT_THRESHOLD == 1 ? 'Singular' : 'Plural'}` };
if (breakouts.length > 0) {
const currentBreakout = breakouts.find(b => b.breakoutId === meetingId);
@ -190,9 +212,10 @@ export default injectIntl(withTracker(({ intl }) => {
breakoutRoom={currentBreakout}
messageDuration={intlMessages.breakoutTimeRemaining}
timeEndedMessage={intlMessages.breakoutWillClose}
alertMessageUnderOneMinute={
intl.formatMessage(intlMessages.alertBreakoutEndsUnderOneMinute)
alertMessage={
intl.formatMessage(msg, {0: REMAINING_TIME_ALERT_THRESHOLD})
}
alertUnderMinutes={REMAINING_TIME_ALERT_THRESHOLD}
/>
);
}
@ -205,7 +228,9 @@ export default injectIntl(withTracker(({ intl }) => {
if (meetingTimeRemaining && Meeting) {
const { timeRemaining } = meetingTimeRemaining;
const { isBreakout } = Meeting.meetingProp;
const underThirtyMin = timeRemaining && timeRemaining <= (30 * 60);
const underThirtyMin = timeRemaining && timeRemaining <= (REMAINING_TIME_THRESHOLD * 60);
const msg = { id: `${intlMessages.alertMeetingEndsUnderMinutes.id}${REMAINING_TIME_ALERT_THRESHOLD == 1 ? 'Singular' : 'Plural'}` };
if (underThirtyMin && !isBreakout) {
data.message = (
@ -213,9 +238,10 @@ export default injectIntl(withTracker(({ intl }) => {
breakoutRoom={meetingTimeRemaining}
messageDuration={intlMessages.meetingTimeRemaining}
timeEndedMessage={intlMessages.meetingWillClose}
alertMessageUnderOneMinute={
intl.formatMessage(intlMessages.alertMeetingEndsUnderOneMinute)
alertMessage={
intl.formatMessage(msg, {0: REMAINING_TIME_ALERT_THRESHOLD})
}
alertUnderMinutes={REMAINING_TIME_ALERT_THRESHOLD}
/>
);
}

View File

@ -38,6 +38,7 @@ const getResponseString = (obj) => {
if (typeof children !== 'string') {
return getResponseString(children[1]);
}
return children;
};
@ -60,7 +61,7 @@ class LiveResult extends PureComponent {
userAnswers = userAnswers.map(id => Service.getUser(id))
.filter(user => user.connectionStatus === 'online')
.map((user) => {
let answer = '-';
let answer = '';
if (responses) {
const response = responses.find(r => r.userId === user.userId);
@ -158,7 +159,7 @@ class LiveResult extends PureComponent {
userCount = userAnswers.length;
userAnswers.map((user) => {
const response = getResponseString(user);
if (response === '-') return user;
if (response === '') return user;
respondedCount += 1;
return user;
});

View File

@ -103,6 +103,12 @@ class PresentationArea extends PureComponent {
currentPresentation,
slidePosition,
webcamDraggableDispatch,
layoutSwapped,
currentSlide,
publishedPoll,
isViewer,
toggleSwapLayout,
restoreOnUpdate,
} = this.props;
const { width: prevWidth, height: prevHeight } = prevProps.slidePosition;
@ -129,6 +135,16 @@ class PresentationArea extends PureComponent {
autoClose: true,
});
}
if (layoutSwapped && restoreOnUpdate && isViewer && currentSlide) {
const slideChanged = currentSlide.id !== prevProps.currentSlide.id;
const positionChanged = slidePosition.viewBoxHeight !== prevProps.slidePosition.viewBoxHeight
|| slidePosition.viewBoxWidth !== prevProps.slidePosition.viewBoxWidth;
const pollPublished = publishedPoll && !prevProps.publishedPoll;
if (slideChanged || positionChanged || pollPublished || presentationChanged) {
toggleSwapLayout();
}
}
}
componentWillUnmount() {
@ -405,6 +421,7 @@ class PresentationArea extends PureComponent {
currentSlide,
slidePosition,
userIsPresenter,
layoutSwapped,
} = this.props;
const {
@ -457,6 +474,7 @@ class PresentationArea extends PureComponent {
width: svgDimensions.width,
height: svgDimensions.height,
textAlign: 'center',
display: layoutSwapped ? 'none' : 'block',
}}
>
{this.renderPresentationClose()}
@ -610,7 +628,7 @@ class PresentationArea extends PureComponent {
<Icon iconName="presentation" />
</div>
</div>
<div className={styles.toastTextContent}>
<div className={styles.toastTextContent} data-test="toastSmallMsg">
<div>{`${intl.formatMessage(intlMessages.changeNotification)}`}</div>
<div className={styles.presentationName}>{`${currentPresentation.name}`}</div>
</div>

View File

@ -1,10 +1,16 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { getSwapLayout, shouldEnableSwapLayout } from '/imports/ui/components/media/service';
import MediaService, { getSwapLayout, shouldEnableSwapLayout } from '/imports/ui/components/media/service';
import { notify } from '/imports/ui/services/notification';
import PresentationAreaService from './service';
import PresentationArea from './component';
import PresentationToolbarService from './presentation-toolbar/service';
import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
import getFromUserSettings from '/imports/ui/services/users-settings';
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
const PresentationAreaContainer = ({ presentationPodIds, mountPresentationArea, ...props }) => (
mountPresentationArea && <PresentationArea {...props} />
@ -14,6 +20,11 @@ export default withTracker(({ podId }) => {
const currentSlide = PresentationAreaService.getCurrentSlide(podId);
const presentationIsDownloadable = PresentationAreaService.isPresentationDownloadable(podId);
const layoutSwapped = getSwapLayout() && shouldEnableSwapLayout();
const isViewer = Users.findOne({ meetingId: Auth.meetingID, userId: Auth.userID }, {
fields: {
role: 1,
},
}).role === ROLE_VIEWER;
let slidePosition;
if (currentSlide) {
@ -36,5 +47,18 @@ export default withTracker(({ podId }) => {
notify,
zoomSlide: PresentationToolbarService.zoomSlide,
podId,
layoutSwapped,
toggleSwapLayout: MediaService.toggleSwapLayout,
publishedPoll: Meetings.findOne({ meetingId: Auth.meetingID }, {
fields: {
publishedPoll: 1,
},
}).publishedPoll,
isViewer,
currentPresentationId: Session.get('currentPresentationId') || null,
restoreOnUpdate: getFromUserSettings(
'bbb_force_restore_presentation_on_new_events',
Meteor.settings.public.presentation.restoreOnUpdate,
),
};
})(PresentationAreaContainer);

View File

@ -191,7 +191,7 @@ export default class Cursor extends Component {
<circle
cx={x}
cy={y}
r={finalRadius}
r={finalRadius === Infinity ? 0 : finalRadius}
fill={fill}
fillOpacity="0.6"
/>

View File

@ -1,9 +1,14 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import cx from 'classnames';
import { styles } from './styles.scss';
export default () => (
const LAYOUT_CONFIG = Meteor.settings.public.layout;
export default (props) => {
const { autoSwapLayout, hidePresentation } = props;
return (
<TransitionGroup>
<CSSTransition
classNames={{
@ -17,7 +22,10 @@ export default () => (
className={styles.contentWrapper}
>
<div className={styles.content}>
<div className={styles.defaultContent}>
<div className={cx(styles.defaultContent, {
[styles.hideContent]: autoSwapLayout && hidePresentation,
})}
>
<p>
<FormattedMessage
id="app.home.greeting"
@ -31,3 +39,4 @@ export default () => (
</CSSTransition>
</TransitionGroup>
);
};

View File

@ -36,6 +36,10 @@
overflow: auto;
}
.hideContent {
visibility: hidden;
}
.appear {
opacity: 0.01;
}

View File

@ -9,7 +9,7 @@ import cx from 'classnames';
import { styles } from './styles.scss';
import ZoomTool from './zoom-tool/component';
import FullscreenButtonContainer from '../../fullscreen-button/container';
import Tooltip from '/imports/ui/components/tooltip/component';
import TooltipContainer from '/imports/ui/components/tooltip/container';
import QuickPollDropdownContainer from '/imports/ui/components/actions-bar/quick-poll-dropdown/container';
import KEY_CODES from '/imports/utils/keyCodes';
@ -274,10 +274,7 @@ class PresentationToolbar extends PureComponent {
data-test="prevSlide"
/>
<Tooltip
title={intl.formatMessage(intlMessages.selectLabel)}
className={styles.presentationBtn}
>
<TooltipContainer title={intl.formatMessage(intlMessages.selectLabel)}>
<select
id="skipSlide"
aria-label={intl.formatMessage(intlMessages.skipSlideLabel)}
@ -292,7 +289,7 @@ class PresentationToolbar extends PureComponent {
>
{this.renderSkipSlideOpts(numberOfSlides)}
</select>
</Tooltip>
</TooltipContainer>
<Button
role="button"
aria-label={nextSlideAriaLabel}
@ -314,6 +311,7 @@ class PresentationToolbar extends PureComponent {
{
!isMobileBrowser
? (
<TooltipContainer>
<ZoomTool
zoomValue={zoom}
change={this.change}
@ -322,6 +320,7 @@ class PresentationToolbar extends PureComponent {
step={STEP}
isMeteorConnected={isMeteorConnected}
/>
</TooltipContainer>
)
: null
}

View File

@ -79,7 +79,9 @@
&:-moz-focusring {
outline: none;
}
border: 0;
background-color: var(--color-off-white);
cursor: pointer;
margin: 0 var(--whiteboard-toolbar-margin) 0 0;
padding: var(--whiteboard-toolbar-padding);
padding-left: var(--whiteboard-toolbar-padding-sm);

Some files were not shown because too many files have changed in this diff Show More