Merge branch 'develop' into issue-9219
This commit is contained in:
commit
e64c0e1c62
33
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
33
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
17
SECURITY.md
Normal 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)
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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"});
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 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() {
|
||||
try {
|
||||
officeManager.start();
|
||||
} catch (OfficeException e) {
|
||||
log.error("Could not start Office Manager", e);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
19
bbb-libreoffice/assets/bbb-libreoffice.service
Normal file
19
bbb-libreoffice/assets/bbb-libreoffice.service
Normal 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
|
||||
|
46
bbb-libreoffice/assets/libreoffice_container.sh
Executable file
46
bbb-libreoffice/assets/libreoffice_container.sh
Executable 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;
|
29
bbb-libreoffice/docker/Dockerfile
Normal file
29
bbb-libreoffice/docker/Dockerfile
Normal 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" ]
|
7
bbb-libreoffice/docker/bbb-libreoffice-entrypoint.sh
Normal file
7
bbb-libreoffice/docker/bbb-libreoffice-entrypoint.sh
Normal 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
59
bbb-libreoffice/install.sh
Executable 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
37
bbb-libreoffice/uninstall.sh
Executable 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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -61,10 +61,11 @@ module.exports = class CallbackEmitter extends EventEmitter {
|
||||
}
|
||||
|
||||
_emitMessage(callback) {
|
||||
let data,requestOptions;
|
||||
let data, requestOptions;
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,9 @@ hooks:
|
||||
permanentURLs:
|
||||
__name: PERMANENT_HOOKS
|
||||
__format: json
|
||||
requestTimeout:
|
||||
__name: REQUEST_TIMEOUT
|
||||
__format: json
|
||||
redis:
|
||||
host: REDIS_HOST
|
||||
port: REDIS_PORT
|
||||
|
@ -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:
|
||||
@ -64,4 +66,4 @@ redis:
|
||||
mappingPrefix: bigbluebutton:webhooks:mapping
|
||||
eventsPrefix: bigbluebutton:webhooks:events
|
||||
userMaps: bigbluebutton:webhooks:userMaps
|
||||
userMapPrefix: bigbluebutton:webhooks:userMap
|
||||
userMapPrefix: bigbluebutton:webhooks:userMap
|
||||
|
5
bbb-webhooks/package-lock.json
generated
5
bbb-webhooks/package-lock.json
generated
@ -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",
|
||||
|
@ -1,2 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=2.3.0-dev
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/bash
|
||||
#
|
||||
# BlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
#
|
||||
@ -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
|
||||
@ -446,7 +447,7 @@ start_bigbluebutton () {
|
||||
|
||||
display_bigbluebutton_status () {
|
||||
units="nginx freeswitch $REDIS_SERVICE bbb-apps-akka bbb-transcode-akka bbb-fsesl-akka"
|
||||
|
||||
|
||||
if [ -f /usr/share/red5/red5-server.jar ]; then
|
||||
units="$units red5"
|
||||
fi
|
||||
@ -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"
|
||||
@ -877,24 +878,10 @@ check_configuration() {
|
||||
echo "# Warning: Detected the value for jnlpUrl is not configured for HTTPS"
|
||||
echo "# /usr/share/red5/webapps/screenshare/WEB-INF/screenshare.properties"
|
||||
echo "#"
|
||||
fi
|
||||
fi
|
||||
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
|
||||
@ -910,12 +897,12 @@ check_configuration() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f /usr/share/red5/red5-server.jar ]; then
|
||||
if find /usr/share /var/lib/red5 -name "*bbb-common-message*" | sed 's/\([^_]*_\).*/\1/g' | sort | uniq -c | grep -v 1 > /dev/null; then echo
|
||||
if [ -f /usr/share/red5/red5-server.jar ]; then
|
||||
if find /usr/share /var/lib/red5 -name "*bbb-common-message*" | sed 's/\([^_]*_\).*/\1/g' | sort | uniq -c | grep -v 1 > /dev/null; then echo
|
||||
echo
|
||||
echo "# Warning: detected multiple bbb-common-message in the same directory"
|
||||
find /usr/share /var/lib/red5 -name "*bbb-common-message*" | sed 's/\([^_]*_\).*/\1/g' | sort | uniq -c | grep -v 1
|
||||
echo
|
||||
find /usr/share /var/lib/red5 -name "*bbb-common-message*" | sed 's/\([^_]*_\).*/\1/g' | sort | uniq -c | grep -v 1
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@ -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
|
||||
@ -1495,7 +1483,7 @@ check_state() {
|
||||
|
||||
CHECK=$(cat ${SERVLET_DIR}/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | grep securitySalt | cut -d= -f2 | sha1sum | cut -d' ' -f1)
|
||||
if [ "$CHECK" == "55b727b294158a877212570c3c0524c2b902a62c" ]; then
|
||||
echo
|
||||
echo
|
||||
echo "#"
|
||||
echo "# Warning: Detected you have the default shared secret. You MUST change your shared"
|
||||
echo "# secret NOW for BigBlueButton to finish starting up. Do either"
|
||||
@ -1513,7 +1501,7 @@ check_state() {
|
||||
fi
|
||||
|
||||
if ! systemctl show-environment | grep LANG= | grep -q UTF-8; then
|
||||
echo
|
||||
echo
|
||||
echo "#"
|
||||
echo "# Warning: Detected that systemctl does not define a UTF-8 language."
|
||||
echo "#"
|
||||
@ -1526,7 +1514,7 @@ check_state() {
|
||||
fi
|
||||
|
||||
if [ "$(stat -c "%U %G" /var/bigbluebutton)" != "bigbluebutton bigbluebutton" ]; then
|
||||
echo
|
||||
echo
|
||||
echo "#"
|
||||
echo "# Warning: The directory"
|
||||
echo "#"
|
||||
@ -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,13 +1678,15 @@ if [ $CHECK ]; then
|
||||
echo " enableListenOnly: $(yq r $HTML5_CONFIG public.kurento.enableListenOnly)"
|
||||
fi
|
||||
|
||||
if ! java -version 2>&1 | grep -q "1.8.0"; then
|
||||
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"
|
||||
echo
|
||||
echo " sudo apt-get install openjdk-8-jdk"
|
||||
echo " update-alternatives --config java"
|
||||
echo " bbb-conf --restart"
|
||||
fi
|
||||
fi
|
||||
|
||||
check_state
|
||||
@ -1864,7 +1854,7 @@ if [ -n "$HOST" ]; then
|
||||
|
||||
echo "Assigning $HOST for servername in /etc/nginx/sites-available/bigbluebutton"
|
||||
$SUDO sed -i "s/server_name .*/server_name $HOST;/g" /etc/nginx/sites-available/bigbluebutton
|
||||
|
||||
|
||||
#
|
||||
# Update configuration for BigBlueButton client (and preserve hostname for chromeExtensionLink if exists)
|
||||
#
|
||||
@ -1883,7 +1873,7 @@ if [ -n "$HOST" ]; then
|
||||
|
||||
echo "Assigning $HOST for publishURI in /var/www/bigbluebutton/client/conf/config.xml"
|
||||
$SUDO sed -i "s/publishURI=\"[^\"]*\"/publishURI=\"$HOST\"/" /var/www/bigbluebutton/client/conf/config.xml
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# Update configuration for BigBlueButton web app
|
||||
@ -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
|
||||
|
35
bigbluebutton-config/cron.hourly/bbb-restart-kms
Normal file
35
bigbluebutton-config/cron.hourly/bbb-restart-kms
Normal 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
|
||||
|
@ -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.
Binary file not shown.
@ -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
|
||||
|
@ -1 +1 @@
|
||||
METEOR@1.9
|
||||
METEOR@1.10.2
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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: '' });
|
||||
}
|
||||
|
@ -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;
|
@ -0,0 +1,2 @@
|
||||
import './methods';
|
||||
import './publishers';
|
@ -0,0 +1,6 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import addConnectionStatus from './methods/addConnectionStatus';
|
||||
|
||||
Meteor.methods({
|
||||
addConnectionStatus,
|
||||
});
|
@ -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);
|
||||
}
|
@ -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)');
|
||||
});
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
@ -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) {
|
||||
|
@ -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}`);
|
||||
});
|
||||
|
@ -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: '' });
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -51,7 +51,7 @@ function publishCurrentUser(...args) {
|
||||
|
||||
Meteor.publish('current-user', publishCurrentUser);
|
||||
|
||||
function users() {
|
||||
function users(role) {
|
||||
if (!this.userId) {
|
||||
return Users.find({ meetingId: '' });
|
||||
}
|
||||
|
@ -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();
|
@ -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');
|
||||
});
|
||||
}
|
@ -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,
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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} />);
|
||||
}
|
||||
return (<ErrorScreen code={codeError} />);
|
||||
return (<MeetingEnded code={codeError} />);
|
||||
}
|
||||
// this.props.annotationsHandler.stop();
|
||||
|
||||
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);
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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}
|
||||
|
@ -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,29 +78,32 @@ class AudioControls extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const label = muted ? intl.formatMessage(intlMessages.unmuteAudio)
|
||||
: intl.formatMessage(intlMessages.muteAudio);
|
||||
|
||||
const toggleMuteBtn = (
|
||||
<Button
|
||||
className={cx(styles.muteToggle, !talking || styles.glow, !muted || styles.btn)}
|
||||
onClick={handleToggleMuteMicrophone}
|
||||
disabled={disable}
|
||||
hideLabel
|
||||
label={label}
|
||||
aria-label={label}
|
||||
color={!muted ? 'primary' : 'default'}
|
||||
ghost={muted}
|
||||
icon={muted ? 'mute' : 'unmute'}
|
||||
size="lg"
|
||||
circle
|
||||
accessKey={shortcuts.togglemute}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<span className={styles.container}>
|
||||
{showMute && isVoiceUser
|
||||
? (
|
||||
<Button
|
||||
className={cx(styles.button, !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)}
|
||||
color={!muted ? 'primary' : 'default'}
|
||||
ghost={muted}
|
||||
icon={muted ? 'mute' : 'unmute'}
|
||||
size="lg"
|
||||
circle
|
||||
accessKey={shortcuts.togglemute}
|
||||
/>
|
||||
) : null}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,16 +58,30 @@ const {
|
||||
joinListenOnly,
|
||||
} = Service;
|
||||
|
||||
export default lockContextContainer(withModalMounter(withTracker(({ mountModal, userLocks }) => ({
|
||||
processToggleMuteFromOutside: arg => processToggleMuteFromOutside(arg),
|
||||
showMute: isConnected() && !isListenOnly() && !isEchoTest() && !userLocks.userMic,
|
||||
muted: isConnected() && !isListenOnly() && isMuted(),
|
||||
inAudio: isConnected() && !isEchoTest(),
|
||||
listenOnly: isConnected() && isListenOnly(),
|
||||
disable: isConnecting() || isHangingUp() || !Meteor.status().connected,
|
||||
talking: isTalking() && !isMuted(),
|
||||
isVoiceUser: isVoiceUser(),
|
||||
handleToggleMuteMicrophone: () => toggleMuteMicrophone(),
|
||||
handleJoinAudio: () => (isConnected() ? joinListenOnly() : mountModal(<AudioModalContainer />)),
|
||||
handleLeaveAudio,
|
||||
}))(AudioControlsContainer)));
|
||||
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(),
|
||||
inAudio: isConnected() && !isEchoTest(),
|
||||
listenOnly: isConnected() && isListenOnly(),
|
||||
disable: isConnecting() || isHangingUp() || !Meteor.status().connected,
|
||||
talking: isTalking() && !isMuted(),
|
||||
isVoiceUser: isVoiceUser(),
|
||||
handleToggleMuteMicrophone: () => toggleMuteMicrophone(),
|
||||
handleJoinAudio: () => (isConnected() ? joinListenOnly() : mountModal(<AudioModalContainer />)),
|
||||
handleLeaveAudio,
|
||||
inputStream: AudioManager.inputStream,
|
||||
isViewer,
|
||||
isPresenter,
|
||||
});
|
||||
})(AudioControlsContainer)));
|
||||
|
@ -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-right: 0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
})),
|
||||
})) : [],
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -31,7 +31,14 @@
|
||||
justify-content: space-around;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
transform: translateZ(0);
|
||||
|
||||
:global(.browser-chrome) & {
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
@include mq($small-only) {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
|
@ -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);
|
@ -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));
|
@ -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%;
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
@ -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.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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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'),
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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;
|
@ -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%;
|
||||
}
|
@ -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)}
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -1,33 +1,42 @@
|
||||
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 () => (
|
||||
<TransitionGroup>
|
||||
<CSSTransition
|
||||
classNames={{
|
||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||
|
||||
export default (props) => {
|
||||
const { autoSwapLayout, hidePresentation } = props;
|
||||
return (
|
||||
<TransitionGroup>
|
||||
<CSSTransition
|
||||
classNames={{
|
||||
appear: styles.appear,
|
||||
appearActive: styles.appearActive,
|
||||
}}
|
||||
appear
|
||||
enter={false}
|
||||
exit={false}
|
||||
timeout={{ enter: 400 }}
|
||||
className={styles.contentWrapper}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.defaultContent}>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="app.home.greeting"
|
||||
description="Message to greet the user."
|
||||
defaultMessage="Your presentation will begin shortly..."
|
||||
/>
|
||||
<br />
|
||||
</p>
|
||||
appear
|
||||
enter={false}
|
||||
exit={false}
|
||||
timeout={{ enter: 400 }}
|
||||
className={styles.contentWrapper}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={cx(styles.defaultContent, {
|
||||
[styles.hideContent]: autoSwapLayout && hidePresentation,
|
||||
})}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="app.home.greeting"
|
||||
description="Message to greet the user."
|
||||
defaultMessage="Your presentation will begin shortly..."
|
||||
/>
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</TransitionGroup>
|
||||
);
|
||||
</CSSTransition>
|
||||
</TransitionGroup>
|
||||
);
|
||||
};
|
||||
|
@ -36,6 +36,10 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.hideContent {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.appear {
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
@ -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,14 +311,16 @@ class PresentationToolbar extends PureComponent {
|
||||
{
|
||||
!isMobileBrowser
|
||||
? (
|
||||
<ZoomTool
|
||||
zoomValue={zoom}
|
||||
change={this.change}
|
||||
minBound={HUNDRED_PERCENT}
|
||||
maxBound={MAX_PERCENT}
|
||||
step={STEP}
|
||||
isMeteorConnected={isMeteorConnected}
|
||||
/>
|
||||
<TooltipContainer>
|
||||
<ZoomTool
|
||||
zoomValue={zoom}
|
||||
change={this.change}
|
||||
minBound={HUNDRED_PERCENT}
|
||||
maxBound={MAX_PERCENT}
|
||||
step={STEP}
|
||||
isMeteorConnected={isMeteorConnected}
|
||||
/>
|
||||
</TooltipContainer>
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user