Merge branch 'master' of https://github.com/bigbluebutton/bigbluebutton into _summit-adjust-screen-reader-trap

This commit is contained in:
KDSBrowne 2019-06-12 14:40:23 +00:00
commit ffecbfa6ac
194 changed files with 7088 additions and 1562 deletions

View File

@ -11,8 +11,11 @@ import org.bigbluebutton.freeswitch.voice.events.ScreenshareStartedEvent;
import org.freeswitch.esl.client.IEslEventListener;
import org.freeswitch.esl.client.transport.event.EslEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ESLEventListener implements IEslEventListener {
private static Logger log = LoggerFactory.getLogger(ESLEventListener.class);
private static final String START_TALKING_EVENT = "start-talking";
private static final String STOP_TALKING_EVENT = "stop-talking";
@ -83,7 +86,24 @@ public class ESLEventListener implements IEslEventListener {
voiceUserId = "v_" + memberId.toString();
}
VoiceUserJoinedEvent pj = new VoiceUserJoinedEvent(voiceUserId, memberId.toString(), confName, callerId, callerIdName, muted, speaking, "none");
String callerUUID = this.getMemberUUIDFromEvent(event);
log.info("Caller joined: conf=" + confName +
",uuid=" + callerUUID +
",memberId=" + memberId +
",callerId=" + callerId +
",callerIdName=" + callerIdName +
",muted=" + muted +
",talking=" + speaking
);
VoiceUserJoinedEvent pj = new VoiceUserJoinedEvent(voiceUserId,
memberId.toString(),
confName,
callerId,
callerIdName,
muted,
speaking,
"none");
conferenceEventListener.handleConferenceEvent(pj);
}
}
@ -100,6 +120,13 @@ public class ESLEventListener implements IEslEventListener {
DeskShareEndedEvent dsEnd = new DeskShareEndedEvent(confName, callerId, callerIdName);
conferenceEventListener.handleConferenceEvent(dsEnd);
} else {
String callerUUID = this.getMemberUUIDFromEvent(event);
log.info("Caller left: conf=" + confName +
",uuid=" + callerUUID +
",memberId=" + memberId +
",callerId=" + callerId +
",callerIdName=" + callerIdName
);
VoiceUserLeftEvent pl = new VoiceUserLeftEvent(memberId.toString(), confName);
conferenceEventListener.handleConferenceEvent(pl);
}
@ -227,6 +254,19 @@ public class ESLEventListener implements IEslEventListener {
return e.getEventHeaders().get("Caller-Caller-ID-Number");
}
private String getMemberUUIDFromEvent(EslEvent e) {
return e.getEventHeaders().get("Caller-Unique-ID");
}
private String getCallerChannelCreateTimeFromEvent(EslEvent e) {
return e.getEventHeaders().get("Caller-Channel-Created-Time");
}
private String getCallerChannelHangupTimeFromEvent(EslEvent e) {
return e.getEventHeaders().get("Caller-Channel-Hangup-Time");
}
private String getCallerIdNameFromEvent(EslEvent e) {
return e.getEventHeaders().get("Caller-Caller-ID-Name");
}

View File

@ -165,10 +165,8 @@ public class FreeswitchApplication implements IDelayedCommandListener{
EjectAllUsersCommand cmd = (EjectAllUsersCommand) command;
manager.ejectAll(cmd);
log.debug("Check if ejecting users success for {}.", cmd.getRoom());
CheckIfConfIsRunningCommand command = new CheckIfConfIsRunningCommand(cmd.getRoom(), cmd.getRequesterId());
delayedCommandSenderService.handleMessage(command, 5000);
} else if (command instanceof TransferUserToMeetingCommand) {
TransferUserToMeetingCommand cmd = (TransferUserToMeetingCommand) command;
manager.tranfer(cmd);

View File

@ -20,6 +20,7 @@ package org.bigbluebutton.freeswitch.voice.freeswitch.actions;
import org.apache.commons.lang3.StringUtils;
import org.bigbluebutton.freeswitch.voice.events.ConferenceEventListener;
import org.bigbluebutton.freeswitch.voice.freeswitch.response.ConferenceMember;
import org.bigbluebutton.freeswitch.voice.freeswitch.response.XMLResponseConferenceListParser;
import org.freeswitch.esl.client.transport.message.EslMessage;
import org.slf4j.Logger;
@ -31,6 +32,7 @@ import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.regex.Matcher;
public class CheckIfConfIsRunningCommand extends FreeswitchCommand {
private static Logger log = LoggerFactory.getLogger(CheckIfConfIsRunningCommand.class);
@ -41,6 +43,7 @@ public class CheckIfConfIsRunningCommand extends FreeswitchCommand {
@Override
public String getCommandArgs() {
log.debug("Check if ejecting users was a success for {}.", room);
return getRoom() + SPACE + "xml_list";
}
@ -50,7 +53,7 @@ public class CheckIfConfIsRunningCommand extends FreeswitchCommand {
String firstLine = response.getBodyLines().get(0);
log.info("Check conference response: " + firstLine);
log.info("Check conference first line response: " + firstLine);
//E.g. Conference 85115 not found
if(!firstLine.startsWith("<?xml")) {
@ -69,9 +72,7 @@ public class CheckIfConfIsRunningCommand extends FreeswitchCommand {
SAXParser sp = spf.newSAXParser();
//Hack turning body lines back into string then to ByteStream.... BLAH!
String responseBody = StringUtils.join(response.getBodyLines(), "\n");
//http://mark.koli.ch/2009/02/resolving-orgxmlsaxsaxparseexception-content-is-not-allowed-in-prolog.html
//This Sux!
responseBody = responseBody.trim().replaceFirst("^([\\W]+)<","<");
@ -79,11 +80,31 @@ public class CheckIfConfIsRunningCommand extends FreeswitchCommand {
ByteArrayInputStream bs = new ByteArrayInputStream(responseBody.getBytes());
sp.parse(bs, confXML);
if (confXML.getConferenceList().size() > 0) {
log.warn("WARNING! Failed to eject all users from conference {}.", room);
Integer numUsers = confXML.getConferenceList().size();
if (numUsers > 0) {
log.info("Check conference response: " + responseBody);
log.warn("WARNING! Failed to eject all users from conf={},numUsers={}.", room, numUsers);
for(ConferenceMember member : confXML.getConferenceList()) {
if ("caller".equals(member.getMemberType())) {
//Foreach found member in conference create a JoinedEvent
String callerId = member.getCallerId();
String callerIdName = member.getCallerIdName();
String voiceUserId = callerIdName;
String uuid = member.getUUID();
log.info("WARNING! User possibly stuck in conference. uuid=" + uuid
+ ",caller=" + callerIdName + ",callerId=" + callerId + ",conf=" + room);
} else if ("recording_node".equals(member.getMemberType())) {
}
}
} else {
log.info("INFO! Successfully ejected all users from conference {}.", room);
}
}catch(SAXException se) {
// System.out.println("Cannot parse repsonce. ", se);
}catch(ParserConfigurationException pce) {

View File

@ -36,8 +36,11 @@ import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.SAXException;
public class GetAllUsersCommand extends FreeswitchCommand {
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GetAllUsersCommand extends FreeswitchCommand {
private static Logger log = LoggerFactory.getLogger(GetAllUsersCommand.class);
public GetAllUsersCommand(String room, String requesterId) {
super(room, requesterId);
}
@ -83,24 +86,38 @@ public class GetAllUsersCommand extends FreeswitchCommand {
ByteArrayInputStream bs = new ByteArrayInputStream(responseBody.getBytes());
sp.parse(bs, confXML);
//Maybe move this to XMLResponseConferenceListParser, sendConfrenceEvents ?
VoiceUserJoinedEvent pj;
Integer numUsers = confXML.getConferenceList().size();
log.info("Num users in conf when starting. conf={},numUsers={}.", room, numUsers);
for(ConferenceMember member : confXML.getConferenceList()) {
//Foreach found member in conference create a JoinedEvent
String callerId = member.getCallerId();
String callerIdName = member.getCallerIdName();
String voiceUserId = callerIdName;
Matcher matcher = CALLERNAME_PATTERN.matcher(callerIdName);
if (matcher.matches()) {
voiceUserId = matcher.group(1).trim();
callerIdName = matcher.group(2).trim();
}
pj = new VoiceUserJoinedEvent(voiceUserId, member.getId().toString(), confXML.getConferenceRoom(),
callerId, callerIdName, member.getMuted(), member.getSpeaking(), "none");
eventListener.handleConferenceEvent(pj);
if (numUsers > 0) {
log.info("Check conference response: " + responseBody);
for(ConferenceMember member : confXML.getConferenceList()) {
if ("caller".equals(member.getMemberType())) {
//Foreach found member in conference create a JoinedEvent
String callerId = member.getCallerId();
String callerIdName = member.getCallerIdName();
String voiceUserId = callerIdName;
String uuid = member.getUUID();
log.info("Conf user. uuid=" + uuid
+ ",caller=" + callerIdName + ",callerId=" + callerId + ",conf=" + room);
Matcher matcher = CALLERNAME_PATTERN.matcher(callerIdName);
if (matcher.matches()) {
voiceUserId = matcher.group(1).trim();
callerIdName = matcher.group(2).trim();
}
VoiceUserJoinedEvent pj = new VoiceUserJoinedEvent(voiceUserId, member.getId().toString(), confXML.getConferenceRoom(),
callerId, callerIdName, member.getMuted(), member.getSpeaking(), "none");
eventListener.handleConferenceEvent(pj);
} else if ("recording_node".equals(member.getMemberType())) {
}
}
} else {
log.info("INFO! Successfully ejected all users from conference {}.", room);
}
}catch(SAXException se) {

View File

@ -32,6 +32,7 @@ public class ConferenceMember {
protected String callerId;
protected Integer joinTime;
protected Integer lastTalking;
protected String memberType;
public Integer getId() {
return memberId;
@ -69,6 +70,10 @@ public class ConferenceMember {
this.uuid = tempVal;
}
public String getUUID() {
return uuid;
}
public void setCallerIdName(String tempVal) {
this.callerIdName = tempVal;
}
@ -85,4 +90,11 @@ public class ConferenceMember {
this.lastTalking = parseInt;
}
public void setMemberType(String memberType) {
this.memberType = memberType;
}
public String getMemberType() {
return memberType;
}
}

View File

@ -92,8 +92,12 @@ public class XMLResponseConferenceListParser extends DefaultHandler {
inFlags = false;
tempVal = "";
if(qName.equalsIgnoreCase("member")) {
String memberType = attributes.getValue("type");
System.out.println("******************* Member Type = " + memberType);
//create a new instance of ConferenceMember
tempMember = new ConferenceMember();
tempMember.setMemberType(memberType);
}
if(qName.equalsIgnoreCase("flags")) {

View File

@ -19,8 +19,10 @@
package org.bigbluebutton.common2.redis;
import java.util.HashMap;
import java.util.Map;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -29,6 +31,7 @@ import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import org.apache.commons.codec.digest.DigestUtils;
public class RedisStorageService extends RedisAwareCommunicator {
@ -56,6 +59,37 @@ public class RedisStorageService extends RedisAwareCommunicator {
log.info("RedisStorageService Stopped");
}
public String generateSingleUseCaptionToken(String recordId, String caption, Long expirySeconds) {
Map<String, String> data = new HashMap<String, String>();
data.put("recordId", recordId);
data.put("caption", caption);
String token = DigestUtils.sha1Hex(recordId + caption + System.currentTimeMillis());
String key = "captions:" + token + ":singleusetoken";
RedisCommands<String, String> commands = connection.sync();
commands.multi();
commands.hmset(key, data);
commands.expire(key, expirySeconds);
commands.exec();
return token;
}
public Boolean validateSingleUseCaptionToken(String token, String recordId, String caption) {
String key = "captions:" + token + ":singleusetoken";
RedisCommands<String, String> commands = connection.sync();
Boolean keyExist = commands.exists(key) == 1;
if (keyExist) {
Map <String, String> data = commands.hgetall(key);
if (data.get("recordId").equals(recordId) && data.get("caption").equals(caption)) {
commands.del(key);
return true;
}
}
return false;
}
public void recordMeetingInfo(String meetingId, Map<String, String> info) {
recordMeeting(Keys.MEETING_INFO + meetingId, info);
}

View File

@ -48,6 +48,7 @@ public class ApiParams {
public static final String PARENT_MEETING_ID = "parentMeetingID";
public static final String PASSWORD = "password";
public static final String RECORD = "record";
public static final String RECORD_ID = "recordID";
public static final String REDIRECT = "redirect";
public static final String SEQUENCE = "sequence";
public static final String VOICE_BRIDGE = "voiceBridge";

View File

@ -403,21 +403,48 @@ public class MeetingService implements MessageListener {
return null;
}
public Boolean validateTextTrackSingleUseToken(String recordId, String caption, String token) {
return recordingService.validateTextTrackSingleUseToken(recordId, caption, token);
}
public String getRecordingTextTracks(String recordId) {
return recordingService.getRecordingTextTracks(recordId);
}
public String putRecordingTextTrack(String recordId, String kind, String lang, File file, String label,
String origFilename, String trackId) {
String origFilename, String trackId, String contentType, String tempFilename) {
Map<String, Object> logData = new HashMap<>();
logData.put("recordId", recordId);
logData.put("kind", kind);
logData.put("lang", lang);
logData.put("label", label);
logData.put("origFilename", origFilename);
logData.put("contentType", contentType);
logData.put("tempFilename", tempFilename);
logData.put("logCode", "recording_captions_uploaded");
logData.put("description", "Captions for recording uploaded.");
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.info(" --analytics-- data={}", logStr);
UploadedTrack track = new UploadedTrack(recordId, kind, lang, label, origFilename, file, trackId,
getCaptionTrackInboxDir());
getCaptionTrackInboxDir(), contentType, tempFilename);
return recordingService.putRecordingTextTrack(track);
}
public String getCaptionTrackInboxDir() {
return recordingService.getCaptionTrackInboxDir();
}
}
public String getCaptionsDir() {
return recordingService.getCaptionsDir();
}
public boolean isRecordingExist(String recordId) {
return recordingService.isRecordingExist(recordId);
}
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters) {
return recordingService.getRecordings2x(idList, states, metadataFilters);

View File

@ -54,6 +54,8 @@ public class RecordingService {
private String recordStatusDir;
private String captionsDir;
private String presentationBaseDir;
private String defaultServerUrl;
private String defaultTextTrackUrl;
private void copyPresentationFile(File presFile, File dlownloadableFile) {
try {
@ -168,8 +170,12 @@ public class RecordingService {
return recs;
}
public Boolean validateTextTrackSingleUseToken(String recordId, String caption, String token) {
return recordingServiceHelper.validateTextTrackSingleUseToken(recordId, caption, token);
}
public String getRecordingTextTracks(String recordId) {
return recordingServiceHelper.getRecordingTextTracks(recordId, captionsDir);
return recordingServiceHelper.getRecordingTextTracks(recordId, captionsDir, getCaptionFileUrlDirectory());
}
public String putRecordingTextTrack(UploadedTrack track) {
@ -242,6 +248,16 @@ public class RecordingService {
return ids;
}
public boolean isRecordingExist(String recordId) {
List<String> publishList = getAllRecordingIds(publishedDir);
List<String> unpublishList = getAllRecordingIds(unpublishedDir);
if (publishList.contains(recordId) || unpublishList.contains(recordId)) {
return true;
}
return false;
}
public boolean existAnyRecording(List<String> idList) {
List<String> publishList = getAllRecordingIds(publishedDir);
List<String> unpublishList = getAllRecordingIds(unpublishedDir);
@ -374,6 +390,14 @@ public class RecordingService {
presentationBaseDir = dir;
}
public void setDefaultServerUrl(String url) {
defaultServerUrl = url;
}
public void setDefaultTextTrackUrl(String url) {
defaultTextTrackUrl = url;
}
public void setPublishedDir(String dir) {
publishedDir = dir;
}
@ -662,7 +686,16 @@ public class RecordingService {
return baseDir;
}
public String getCaptionTrackInboxDir() {
return captionsDir + File.separatorChar + "inbox";
}
public String getCaptionTrackInboxDir() {
return captionsDir + File.separatorChar + "inbox";
}
public String getCaptionsDir() {
return captionsDir;
}
public String getCaptionFileUrlDirectory() {
return defaultTextTrackUrl + "/textTrack/";
}
}

View File

@ -16,8 +16,12 @@ public class RecordingMetadataReaderHelper {
private RecordingServiceGW recordingServiceGW;
public String getRecordingTextTracks(String recordId, String captionsDir) {
return recordingServiceGW.getRecordingTextTracks(recordId, captionsDir);
public Boolean validateTextTrackSingleUseToken(String recordId, String caption, String token) {
return recordingServiceGW.validateTextTrackSingleUseToken(recordId, caption, token);
}
public String getRecordingTextTracks(String recordId, String captionsDir, String captionsBaseUrl) {
return recordingServiceGW.getRecordingTextTracks(recordId, captionsDir, captionsBaseUrl);
}
public String putRecordingTextTrack(UploadedTrack track) {

View File

@ -13,6 +13,7 @@ public interface RecordingServiceGW {
String getRecordings2x(ArrayList<RecordingMetadata> recs);
Option<RecordingMetadata> getRecordingMetadata(File xml);
boolean saveRecordingMetadata(File xml, RecordingMetadata metadata);
String getRecordingTextTracks(String recordId, String captionsDir);
boolean validateTextTrackSingleUseToken(String recordId, String caption, String token);
String getRecordingTextTracks(String recordId, String captionsDir, String captionBasUrl);
String putRecordingTextTrack(UploadedTrack track);
}

View File

@ -34,6 +34,7 @@ import org.bigbluebutton.presentation.FileTypeConstants;
import org.bigbluebutton.presentation.ImageResizer;
import org.bigbluebutton.presentation.ImageToSwfSlide;
import org.bigbluebutton.presentation.PageConverter;
import org.bigbluebutton.presentation.PngCreator;
import org.bigbluebutton.presentation.SvgImageCreator;
import org.bigbluebutton.presentation.TextFileCreator;
import org.bigbluebutton.presentation.ThumbnailCreator;
@ -52,10 +53,14 @@ public class ImageToSwfSlidesGenerationService {
private SvgImageCreator svgImageCreator;
private ThumbnailCreator thumbnailCreator;
private TextFileCreator textFileCreator;
private PngCreator pngCreator;
private ImageResizer imageResizer;
private Long maxImageSize;
private long MAX_CONVERSION_TIME = 5*60*1000L;
private String BLANK_SLIDE;
private boolean swfSlidesRequired;
private boolean svgImagesRequired;
private boolean generatePngs;
public ImageToSwfSlidesGenerationService() {
int numThreads = Runtime.getRuntime().availableProcessors();
@ -65,15 +70,24 @@ public class ImageToSwfSlidesGenerationService {
public void generateSlides(UploadedPresentation pres) {
pres.setNumberOfPages(1); // There should be only one image to convert.
if (pres.getNumberOfPages() > 0) {
PageConverter pageConverter = determinePageConverter(pres);
convertImageToSwf(pres, pageConverter);
}
if (swfSlidesRequired) {
if (pres.getNumberOfPages() > 0) {
PageConverter pageConverter = determinePageConverter(pres);
convertImageToSwf(pres, pageConverter);
}
}
/* adding accessibility */
createTextFiles(pres);
createThumbnails(pres);
createSvgImages(pres);
if (svgImagesRequired) {
createSvgImages(pres);
}
if (generatePngs) {
createPngImages(pres);
}
notifier.sendConversionCompletedMessage(pres);
}
@ -105,6 +119,10 @@ public class ImageToSwfSlidesGenerationService {
svgImageCreator.createSvgImages(pres);
}
private void createPngImages(UploadedPresentation pres) {
pngCreator.createPng(pres);
}
private void convertImageToSwf(UploadedPresentation pres, PageConverter pageConverter) {
int numPages = pres.getNumberOfPages();
// A better implementation is described at the link below
@ -178,7 +196,7 @@ public class ImageToSwfSlidesGenerationService {
});
}
}
public void setJpgPageConverter(PageConverter converter) {
this.jpgToSwfConverter = converter;
}
@ -194,14 +212,31 @@ public class ImageToSwfSlidesGenerationService {
public void setThumbnailCreator(ThumbnailCreator thumbnailCreator) {
this.thumbnailCreator = thumbnailCreator;
}
public void setTextFileCreator(TextFileCreator textFileCreator) {
this.textFileCreator = textFileCreator;
}
public void setPngCreator(PngCreator pngCreator) {
this.pngCreator = pngCreator;
}
public void setSvgImageCreator(SvgImageCreator svgImageCreator) {
this.svgImageCreator = svgImageCreator;
}
public void setGeneratePngs(boolean generatePngs) {
this.generatePngs = generatePngs;
}
public void setSwfSlidesRequired(boolean swf) {
this.swfSlidesRequired = swf;
}
public void setSvgImagesRequired(boolean svg) {
this.svgImagesRequired = svg;
}
public void setMaxConversionTime(int minutes) {
MAX_CONVERSION_TIME = minutes * 60 * 1000L;
}

View File

@ -50,7 +50,7 @@ public class PdfToSwfSlidesGenerationService {
private PageConverter pdfToSwfConverter;
private ExecutorService executor;
private ThumbnailCreator thumbnailCreator;
private PngCreator pngCreator;
private PngCreator pngCreator;
private TextFileCreator textFileCreator;
private SvgImageCreator svgImageCreator;
@ -71,9 +71,10 @@ public class PdfToSwfSlidesGenerationService {
// Only create SWF files if the configuration requires it
if (swfSlidesRequired) {
convertPdfToSwf(pres);
createThumbnails(pres);
}
/* adding accessibility */
createThumbnails(pres);
createTextFiles(pres);
// only create SVG images if the configuration requires it

View File

@ -38,6 +38,19 @@ class BbbWebApiGWApp(
val redisPass = if (redisPassword != "") Some(redisPassword) else None
val redisConfig = RedisConfig(redisHost, redisPort, redisPass, redisExpireKey)
var redisStorage = new RedisStorageService()
redisStorage.setHost(redisConfig.host)
redisStorage.setPort(redisConfig.port)
val redisPassStr = redisConfig.password match {
case Some(pass) => pass
case None => ""
}
redisStorage.setPassword(redisPassStr)
redisStorage.setExpireKey(redisConfig.expireKey)
redisStorage.setClientName("BbbWebRedisStore")
redisStorage.start()
private val redisPublisher = new RedisPublisher(system, "BbbWebPub", redisConfig)
private val msgSender: MessageSender = new MessageSender(redisPublisher)
@ -274,4 +287,13 @@ class BbbWebApiGWApp(
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
}
}
/*** Caption API ***/
def generateSingleUseCaptionToken(recordId: String, caption: String, expirySeconds: Long): String = {
redisStorage.generateSingleUseCaptionToken(recordId, caption, expirySeconds)
}
def validateSingleUseCaptionToken(token: String, meetingId: String, caption: String): Boolean = {
redisStorage.validateSingleUseCaptionToken(token, meetingId, caption)
}
}

View File

@ -12,21 +12,25 @@ case class UploadedTrack(
origFilename: String,
track: File,
trackId: String,
inboxDir: String
inboxDir: String,
contentType: String,
tempFilename: String
)
case class UploadedTrackInfo(
recordId: String,
kind: String,
lang: String,
label: String,
origFilename: String
record_id: String,
kind: String,
lang: String,
label: String,
original_filename: String,
content_type: String,
temp_filename: String
)
case class Track(
href: String,
kind: String,
lang: String,
label: String,
source: String,
href: String
lang: String,
source: String
)
case class GetRecTextTracksResult(
returncode: String,

View File

@ -4,23 +4,27 @@ import java.io.{ File, FileOutputStream, FileWriter, IOException }
import java.nio.channels.Channels
import java.nio.charset.StandardCharsets
import java.util
import java.nio.file.{ Files, Paths }
import com.google.gson.Gson
import org.bigbluebutton.api.domain.RecordingMetadata
import org.bigbluebutton.api2.RecordingServiceGW
import org.bigbluebutton.api2.{ BbbWebApiGWApp, RecordingServiceGW }
import org.bigbluebutton.api2.domain._
import scala.xml.{ Elem, PrettyPrinter, XML }
import scala.collection.JavaConverters._
import scala.collection.mutable.{ Buffer, ListBuffer, Map }
import scala.collection.Iterable
import java.io.IOException
import java.nio.charset.Charset
import java.nio.file.Files
import java.nio.file.Paths
class RecMetaXmlHelper extends RecordingServiceGW with LogHelper {
import com.google.gson.internal.LinkedTreeMap
import scala.util.Try
class RecMetaXmlHelper(gw: BbbWebApiGWApp) extends RecordingServiceGW with LogHelper {
val SUCCESS = "SUCCESS"
val FAILED = "FAILED"
@ -188,19 +192,43 @@ class RecMetaXmlHelper extends RecordingServiceGW with LogHelper {
}
}
def getRecordingTextTracks(recordId: String, captionsDir: String): String = {
def validateTextTrackSingleUseToken(recordId: String, caption: String, token: String): Boolean = {
gw.validateSingleUseCaptionToken(token, recordId, caption)
}
def getRecordingsCaptionsJson(recordId: String, captionsDir: String, captionBaseUrl: String): String = {
val gson = new Gson()
var returnResponse: String = ""
val captionsFilePath = captionsDir + File.separatorChar + recordId + File.separatorChar + CAPTIONS_FILE
readCaptionJsonFile(captionsFilePath, StandardCharsets.UTF_8) match {
case Some(captions) =>
val ctracks = gson.fromJson(captions, classOf[util.ArrayList[Track]])
val result1 = GetRecTextTracksResult(SUCCESS, ctracks)
val response1 = GetRecTextTracksResp(result1)
val respText1 = gson.toJson(response1)
val ctracks = gson.fromJson(captions, classOf[java.util.List[LinkedTreeMap[String, String]]])
returnResponse = respText1
val list = new util.ArrayList[Track]()
val it = ctracks.iterator()
while (it.hasNext()) {
val mapTrack = it.next()
val caption = mapTrack.get("kind") + "_" + mapTrack.get("lang") + ".vtt"
val singleUseToken = gw.generateSingleUseCaptionToken(recordId, caption, 60 * 60)
list.add(new Track(
// captionBaseUrl contains the '/' so no need to put one before singleUseToken
href = captionBaseUrl + singleUseToken + '/' + recordId + '/' + caption,
kind = mapTrack.get("kind"),
label = mapTrack.get("label"),
lang = mapTrack.get("lang"),
source = mapTrack.get("source")
))
}
val textTracksResult = GetRecTextTracksResult(SUCCESS, list)
val textTracksResponse = GetRecTextTracksResp(textTracksResult)
val textTracksJson = gson.toJson(textTracksResponse)
// parse(textTracksJson).transformField{case JField(x, v) if x == "value" && v == JString("Company")=> JField("value1",JString("Company1"))}
returnResponse = textTracksJson
case None =>
val resFailed = GetRecTextTracksResultFailed(FAILED, "noCaptionsFound", "No captions found for " + recordId)
val respFailed = GetRecTextTracksRespFailed(resFailed)
@ -212,6 +240,21 @@ class RecMetaXmlHelper extends RecordingServiceGW with LogHelper {
returnResponse
}
def getRecordingTextTracks(recordId: String, captionsDir: String, captionBaseUrl: String): String = {
val gson = new Gson()
var returnResponse: String = ""
val recordingPath = captionsDir + File.separatorChar + recordId
if (!Files.exists(Paths.get(recordingPath))) {
val resFailed = GetRecTextTracksResultFailed(FAILED, "noRecordings", "No recording found for " + recordId)
val respFailed = GetRecTextTracksRespFailed(resFailed)
returnResponse = gson.toJson(respFailed)
} else {
returnResponse = getRecordingsCaptionsJson(recordId, captionsDir, captionBaseUrl)
}
returnResponse
}
def saveCaptionsFile(captionsDir: String, captionsTracks: String): Boolean = {
val path = captionsDir + File.separatorChar + CAPTIONS_FILE
val fileWriter = new FileWriter(path)
@ -232,18 +275,25 @@ class RecMetaXmlHelper extends RecordingServiceGW with LogHelper {
}
}
def mv(oldName: String, newName: String) =
Try(new File(oldName).renameTo(new File(newName))).getOrElse(false)
def saveTrackInfoFile(trackInfoJson: String, trackInfoFilePath: String): Boolean = {
// Need to create intermediate file to prevent race where the file is processed before
// contents have been written.
val tempTrackInfoFilePath = trackInfoFilePath + ".tmp"
var result = false
val fileWriter = new FileWriter(trackInfoFilePath)
val fileWriter = new FileWriter(tempTrackInfoFilePath)
try {
fileWriter.write(trackInfoJson)
result = true
} catch {
case ioe: IOException =>
logger.info("Failed to write caption.json {}", trackInfoFilePath)
logger.info("Failed to write caption.json {}", tempTrackInfoFilePath)
result = false
case ex: Exception =>
logger.info("Exception while writing {}", trackInfoFilePath)
logger.info("Exception while writing {}", tempTrackInfoFilePath)
logger.info("Exception details: {}", ex.getMessage)
result = false
} finally {
@ -251,6 +301,11 @@ class RecMetaXmlHelper extends RecordingServiceGW with LogHelper {
fileWriter.close()
}
if (result) {
// Rename so that the captions processor will pick up the uploaded captions.
result = mv(tempTrackInfoFilePath, trackInfoFilePath)
}
result
}
@ -258,11 +313,13 @@ class RecMetaXmlHelper extends RecordingServiceGW with LogHelper {
val trackInfoFilePath = track.inboxDir + File.separatorChar + track.trackId + "-track.json"
val trackInfo = new UploadedTrackInfo(
recordId = track.recordId,
record_id = track.recordId,
kind = track.kind,
lang = track.lang,
label = track.label,
origFilename = track.origFilename
original_filename = track.origFilename,
temp_filename = track.tempFilename,
content_type = track.contentType
)
val gson = new Gson()

View File

@ -34,5 +34,13 @@
alias="${cert.alias}" />
</target>
<target name="resign-ffmpeg-jar" depends="get-alias-name">
<signjar jar="workdir/ffmpeg-linux-x86-svc2.jar"
storetype="pkcs12"
keystore="${cert.name}"
storepass="${cert.password}"
alias="${cert.alias}" />
</target>
</project>

View File

@ -0,0 +1,10 @@
JAR=../../../../app/jws/lib/ffmpeg-linux-x86-svc2.jar
if [ -d "workdir" ]; then
rm -rf workdir
fi
mkdir workdir
cp $JAR workdir
ant resign-ffmpeg-jar
cp workdir/ffmpeg-linux-x86-svc2.jar ../../../../app/jws/lib/
rm -rf workdir

View File

@ -34,5 +34,13 @@
alias="${cert.alias}" />
</target>
<target name="resign-ffmpeg-jar" depends="get-alias-name">
<signjar jar="workdir/ffmpeg-linux-x86_64-svc2.jar"
storetype="pkcs12"
keystore="${cert.name}"
storepass="${cert.password}"
alias="${cert.alias}" />
</target>
</project>

View File

@ -0,0 +1,10 @@
JAR=../../../../app/jws/lib/ffmpeg-linux-x86_64-svc2.jar
if [ -d "workdir" ]; then
rm -rf workdir
fi
mkdir workdir
cp $JAR workdir
ant resign-ffmpeg-jar
cp workdir/ffmpeg-linux-x86_64-svc2.jar ../../../../app/jws/lib/
rm -rf workdir

View File

@ -34,5 +34,13 @@
alias="${cert.alias}" />
</target>
<target name="resign-ffmpeg-jar" depends="get-alias-name">
<signjar jar="workdir/ffmpeg-macosx-x86_64-svc2.jar"
storetype="pkcs12"
keystore="${cert.name}"
storepass="${cert.password}"
alias="${cert.alias}" />
</target>
</project>

View File

@ -0,0 +1,10 @@
JAR=../../../../app/jws/lib/ffmpeg-macosx-x86_64-svc2.jar
if [ -d "workdir" ]; then
rm -rf workdir
fi
mkdir workdir
cp $JAR workdir
ant resign-ffmpeg-jar
cp workdir/ffmpeg-macosx-x86_64-svc2.jar ../../../../app/jws/lib/
rm -rf workdir

View File

@ -34,5 +34,13 @@
alias="${cert.alias}" />
</target>
<target name="resign-ffmpeg-jar" depends="get-alias-name">
<signjar jar="workdir/ffmpeg-win-x86-svc2.jar"
storetype="pkcs12"
keystore="${cert.name}"
storepass="${cert.password}"
alias="${cert.alias}" />
</target>
</project>

View File

@ -0,0 +1,10 @@
JAR=../../../../app/jws/lib/ffmpeg-win-x86-svc2.jar
if [ -d "workdir" ]; then
rm -rf workdir
fi
mkdir workdir
cp $JAR workdir
ant resign-ffmpeg-jar
cp workdir/ffmpeg-win-x86-svc2.jar ../../../../app/jws/lib/
rm -rf workdir

View File

@ -34,5 +34,12 @@
alias="${cert.alias}" />
</target>
<target name="resign-ffmpeg-jar" depends="get-alias-name">
<signjar jar="workdir/ffmpeg-win-x86_64-svc2.jar"
storetype="pkcs12"
keystore="${cert.name}"
storepass="${cert.password}"
alias="${cert.alias}" />
</target>
</project>

View File

@ -0,0 +1,10 @@
JAR=../../../../app/jws/lib/ffmpeg-win-x86_64-svc2.jar
if [ -d "workdir" ]; then
rm -rf workdir
fi
mkdir workdir
cp $JAR workdir
ant resign-ffmpeg-jar
cp workdir/ffmpeg-win-x86_64-svc2.jar ../../../../app/jws/lib/
rm -rf workdir

View File

@ -1,16 +1,16 @@
FFMPEG=ffmpeg-3.0.2-1.2-windows-x86_64-svc2.jar
mkdir workdir
cp $FFMPEG workdir/ffmpeg-windows-x86_64.jar
rm -rf src
mkdir -p src/main/resources
mkdir -p src/main/java
cd workdir
jar xvf ffmpeg-windows-x86_64.jar
cp org/bytedeco/javacpp/windows-x86_64/*.dll ../src/main/resources
cd ..
rm -rf workdir
gradle jar
cp build/libs/ffmpeg-windows-x86_64-0.0.1.jar ../../unsigned-jars/ffmpeg-win-x86_64-svc2-unsigned.jar
ant sign-jar
cp build/libs/ffmpeg-windows-x86_64-0.0.1.jar ../../../../app/jws/lib/ffmpeg-win-x86_64-svc2.jar
rm -rf src
FFMPEG=ffmpeg-3.0.2-1.2-windows-x86_64-svc2.jar
mkdir workdir
cp $FFMPEG workdir/ffmpeg-windows-x86_64.jar
rm -rf src
mkdir -p src/main/resources
mkdir -p src/main/java
cd workdir
jar xvf ffmpeg-windows-x86_64.jar
cp org/bytedeco/javacpp/windows-x86_64/*.dll ../src/main/resources
cd ..
rm -rf workdir
gradle jar
cp build/libs/ffmpeg-windows-x86_64-0.0.1.jar ../../unsigned-jars/ffmpeg-win-x86_64-svc2-unsigned.jar
ant sign-jar
cp build/libs/ffmpeg-windows-x86_64-0.0.1.jar ../../../../app/jws/lib/ffmpeg-win-x86_64-svc2.jar
rm -rf src

View File

@ -50,4 +50,13 @@
alias="${cert.alias}" />
</target>
<target name="sign-screenshare-jar" depends="get-alias-name">
<signjar jar="workdir/javacv-screenshare-0.0.1.jar"
storetype="pkcs12"
keystore="${cert.name}"
storepass="${cert.password}"
alias="${cert.alias}" />
</target>
</project>

View File

@ -0,0 +1,12 @@
JAR=../../../app/jws/lib/ffmpeg.jar
if [ -d "workdir" ]; then
rm -rf workdir
fi
mkdir workdir
cp $JAR workdir
cd workdir
cd ..
ant sign-ffmpeg-jar
cp workdir/ffmpeg.jar ../../../app/jws/lib/
rm -rf workdir

View File

@ -0,0 +1,12 @@
JAR=javacv-screenshare-0.0.1.jar
if [ -d "workdir" ]; then
rm -rf workdir
fi
mkdir workdir
cp $JAR workdir
cd workdir
cd ..
ant sign-screenshare-jar
cp workdir/javacv-screenshare-0.0.1.jar ../../../app/jws/lib/
rm -rf workdir

View File

@ -92,14 +92,14 @@ source /etc/bigbluebutton/bigbluebutton-release
if [ -f /etc/centos-release ]; then
DISTRIB_ID=centos
TOMCAT_SERVICE=tomcat
TOMCAT_DIR=/var/lib/$TOMCAT_SERVICE
TOMCAT_USER=tomcat
TOMCAT_DIR=/var/lib/$TOMCAT_USER
SERVLET_LOGS=/usr/share/tomcat/logs
REDIS_SERVICE=redis.service
else
. /etc/lsb-release # Get value for DISTRIB_ID
TOMCAT_SERVICE=tomcat7
TOMCAT_DIR=/var/lib/$TOMCAT_SERVICE
TOMCAT_USER=tomcat7
TOMCAT_DIR=/var/lib/$TOMCAT_USER
SERVLET_LOGS=$TOMCAT_DIR/logs
REDIS_SERVICE=redis-server
fi
@ -328,7 +328,15 @@ stop_bigbluebutton () {
BBB_WEB=bbb-web
fi
systemctl stop red5 $TOMCAT_SERVICE nginx freeswitch $REDIS_SERVICE bbb-apps-akka $BBB_TRANSCODE_AKKA bbb-fsesl-akka bbb-rap-archive-worker.service bbb-rap-process-worker.service bbb-rap-publish-worker.service bbb-rap-sanity-worker.service bbb-record-core.timer $HTML5 $WEBHOOKS $ETHERPAD $BBB_WEB
if [ -d $TOMCAT_DIR ]; then
TOMCAT_SERVICE=$TOMCAT_USER
fi
if [ -d $RED5_DIR ]; then
BBB_APPS_AKKA_SERVICE=bbb-apps-akka
fi
systemctl stop red5 $TOMCAT_SERVICE $BBB_APPS_AKKA_SERVICE nginx freeswitch $REDIS_SERVICE bbb-apps-akka $BBB_TRANSCODE_AKKA bbb-fsesl-akka bbb-rap-archive-worker.service bbb-rap-process-worker.service bbb-rap-publish-worker.service bbb-rap-sanity-worker.service bbb-record-core.timer $HTML5 $WEBHOOKS $ETHERPAD $BBB_WEB
}
start_bigbluebutton () {
@ -431,7 +439,7 @@ display_bigbluebutton_status () {
units="red5 nginx freeswitch $REDIS_SERVICE bbb-apps-akka bbb-transcode-akka bbb-fsesl-akka"
if [ -d $TOMCAT_DIR ]; then
units="$units $TOMCAT_SERVICE"
units="$units $TOMCAT_USER"
fi
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
@ -936,14 +944,14 @@ check_state() {
if ! netstat -ant | grep '8090' > /dev/null; then
print_header
NOT_RUNNING_APPS="${NOT_RUNNING_APPS} ${TOMCAT_SERVICE} or grails"
NOT_RUNNING_APPS="${NOT_RUNNING_APPS} ${TOMCAT_USER} or grails"
else
if ps aux | ps -aef | grep -v grep | grep grails | grep run-app > /dev/null; then
print_header
RUNNING_APPS="${RUNNING_APPS} Grails"
echo "# ${TOMCAT_SERVICE}: noticed you are running grails run-app instead of ${TOMCAT_SERVICE}"
echo "# ${TOMCAT_USER}: noticed you are running grails run-app instead of ${TOMCAT_USER}"
else
RUNNING_APPS="${RUNNING_APPS} ${TOMCAT_SERVICE}"
RUNNING_APPS="${RUNNING_APPS} ${TOMCAT_USER}"
fi
fi

View File

@ -35,6 +35,7 @@
# 2016-07-02 FFD Updates for 1.1-beta
# 2016-10-17 GTR Stricter rule for detection of recording directories names
# 2017-04-28 FFD Updated references to systemd processing units
# 2019-05-13 GTR Delete caption files
#set -e
#set -x
@ -372,6 +373,10 @@ if [ $DELETE ]; then
rm -rf /var/log/bigbluebutton/$type/*$MEETING_ID*
done
rm -rf /var/bigbluebutton/captions/$MEETING_ID*
rm -f /var/bigbluebutton/inbox/$MEETING_ID*.json
rm -f /var/bigbluebutton/inbox/$MEETING_ID*.txt
rm -rf /var/bigbluebutton/recording/raw/$MEETING_ID*
rm -rf /usr/share/red5/webapps/video/streams/$MEETING_ID
@ -402,7 +407,9 @@ if [ $DELETEALL ]; then
done
rm -rf /var/bigbluebutton/recording/raw/*
rm -f /var/bigbluebutton/captions/inbox/*
find /usr/share/red5/webapps/video/streams -name "*.flv" -exec rm '{}' \;
find /usr/share/red5/webapps/video-broadcast/streams -name "*.flv" -exec rm '{}' \;
rm -f /var/bigbluebutton/screenshare/*.flv
@ -411,6 +418,7 @@ if [ $DELETEALL ]; then
for meeting in $(ls /var/bigbluebutton | grep "^[0-9a-f]\{40\}-[[:digit:]]\{13\}$"); do
echo "deleting: $meeting"
rm -rf /var/bigbluebutton/$meeting
rm -rf /var/bigbluebutton/captions/$meeting
done
fi

View File

@ -44,14 +44,59 @@ export default function sendAnnotation(credentials, annotation) {
// and then slide/presentation changes, the user lost presenter rights,
// or multi-user whiteboard gets turned off
// So we allow the last "DRAW_END" message to pass through, to finish the shape.
const allowed = isPodPresenter(meetingId, whiteboardId, requesterUserId) ||
getMultiUserStatus(meetingId, whiteboardId) ||
isLastMessage(meetingId, annotation, requesterUserId);
const allowed = isPodPresenter(meetingId, whiteboardId, requesterUserId)
|| getMultiUserStatus(meetingId, whiteboardId)
|| isLastMessage(meetingId, annotation, requesterUserId);
if (!allowed) {
throw new Meteor.Error('not-allowed', `User ${requesterUserId} is not allowed to send an annotation`);
}
if (annotation.annotationType === 'text') {
check(annotation, {
id: String,
status: String,
annotationType: String,
annotationInfo: {
x: Number,
y: Number,
fontColor: Number,
calcedFontSize: Number,
textBoxWidth: Number,
text: String,
textBoxHeight: Number,
id: String,
whiteboardId: String,
status: String,
fontSize: Number,
dataPoints: String,
type: String,
},
wbId: String,
userId: String,
position: Number,
});
} else {
check(annotation, {
id: String,
status: String,
annotationType: String,
annotationInfo: {
color: Number,
thickness: Number,
points: Array,
id: String,
whiteboardId: String,
status: String,
type: String,
dimensions: Match.Maybe([Number]),
},
wbId: String,
userId: String,
position: Number,
});
}
const payload = {
annotation,
};

View File

@ -89,7 +89,10 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
};
const onFail = (error) => {
const { reason } = error;
let reason = 'Undefined';
if (error) {
reason = error.reason || error.id || error;
}
this.callback({
status: this.baseCallStates.failed,
error: this.baseErrorCodes.CONNECTION_ERROR,

View File

@ -2,7 +2,9 @@ import browser from 'browser-detect';
import BaseAudioBridge from './base';
import logger from '/imports/startup/client/logger';
import { fetchStunTurnServers } from '/imports/utils/fetchStunTurnServers';
import { isUnifiedPlan, toUnifiedPlan, toPlanB } from '/imports/utils/sdpUtils';
import {
isUnifiedPlan, toUnifiedPlan, toPlanB, stripMDnsCandidates,
} from '/imports/utils/sdpUtils';
const MEDIA = Meteor.settings.public.media;
const MEDIA_TAG = MEDIA.mediaTag;
@ -40,6 +42,7 @@ export default class SIPBridge extends BaseAudioBridge {
window.isUnifiedPlan = isUnifiedPlan;
window.toUnifiedPlan = toUnifiedPlan;
window.toPlanB = toPlanB;
window.stripMDnsCandidates = stripMDnsCandidates;
}
static parseDTMF(message) {
@ -182,7 +185,7 @@ export default class SIPBridge extends BaseAudioBridge {
// transceivers - prlanzarin 2019/05/21
const browserUA = window.navigator.userAgent.toLocaleLowerCase();
const isSafariWebview = ((browserUA.indexOf('iphone') > -1
|| browserUA.indexOf('ipad') > -1) && browserUA.indexOf('safari') == -1);
|| browserUA.indexOf('ipad') > -1) && browserUA.indexOf('safari') === -1);
// Second UA check to get all Safari browsers to enable Unified Plan <-> PlanB
// translation

View File

@ -3,7 +3,7 @@ import Logger from '/imports/startup/server/logger';
import editCaptions from '/imports/api/captions/server/methods/editCaptions';
import { check } from 'meteor/check';
export default function padUpdate(padId, data, revs) {
export default function updatePad(padId, data, revs) {
check(padId, String);
check(data, String);
check(revs, Number);

View File

@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor';
const Note = new Mongo.Collection('note');
if (Meteor.isServer) {
Note._ensureIndex({ meetingId: 1 });
Note._ensureIndex({ meetingId: 1, noteId: 1 });
}
export default Note;

View File

@ -0,0 +1,5 @@
import RedisPubSub from '/imports/startup/server/redis';
import { processForNotePadOnly } from '/imports/api/note/server/helpers';
import handlePadUpdate from './handlers/padUpdate';
RedisPubSub.on('PadUpdateSysMsg', processForNotePadOnly(handlePadUpdate));

View File

@ -0,0 +1,12 @@
import { check } from 'meteor/check';
import updateNote from '/imports/api/note/server/modifiers/updateNote';
export default function handlePadUpdate({ body }) {
const { pad, revs } = body;
const { id } = pad;
check(id, String);
check(revs, Number);
updateNote(id, revs);
}

View File

@ -4,6 +4,7 @@ import { hashFNV32a } from '/imports/api/common/server/helpers';
const ETHERPAD = Meteor.settings.private.etherpad;
const NOTE_CONFIG = Meteor.settings.public.note;
const BASE_URL = `http://${ETHERPAD.host}:${ETHERPAD.port}/api/${ETHERPAD.version}`;
const TOKEN = '_';
const createPadURL = padId => `${BASE_URL}/createPad?apikey=${ETHERPAD.apikey}&padID=${padId}`;
@ -26,10 +27,26 @@ const getDataFromResponse = (data, key) => {
return null;
};
const isNotePad = (padId) => {
return padId.search(TOKEN);
};
const processForNotePadOnly = fn => (message, ...args) => {
const { body } = message;
const { pad } = body;
const { id } = pad;
check(id, String);
if (isNotePad(id)) return fn(message, ...args);
return () => {};
};
export {
generateNoteId,
createPadURL,
getReadOnlyIdURL,
isEnabled,
getDataFromResponse,
processForNotePadOnly,
};

View File

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

View File

@ -9,12 +9,14 @@ export default function addNote(meetingId, noteId, readOnlyNoteId) {
const selector = {
meetingId,
noteId,
};
const modifier = {
meetingId,
noteId,
readOnlyNoteId,
revs: 0,
};
const cb = (err, numChanged) => {

View File

@ -0,0 +1,28 @@
import Note from '/imports/api/note';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
export default function updateNote(noteId, revs) {
check(noteId, String);
check(revs, Number);
const selector = {
noteId,
};
const modifier = {
$set: {
revs,
},
};
const cb = (err) => {
if (err) {
return Logger.error(`Updating note pad: ${err}`);
}
return Logger.verbose(`Update note pad=${noteId} revs=${revs}`);
};
return Note.update(selector, modifier, { multi: true }, cb);
}

View File

@ -18,17 +18,22 @@ export default function setPresentation(credentials, presentationId, podId) {
meetingId,
id: presentationId,
podId,
current: true,
});
if (currentPresentation && currentPresentation.id === presentationId) {
return Promise.resolve();
if (currentPresentation) {
if (currentPresentation.current) {
return Promise.resolve();
}
const payload = {
presentationId,
podId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}
const payload = {
presentationId,
podId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
// did not find presentation with such id. abandon
// return Promise.resolve(); // will close the uploading modal
throw new Meteor.Error('presentation-not-found', `Did not find a presentation with id ${presentationId} in method setPresentation`);
}

View File

@ -33,7 +33,7 @@ const propTypes = {
const defaultProps = {
locale: undefined,
approved: undefined,
approved: false,
meetingExist: false,
subscriptionsReady: false,
};
@ -227,18 +227,8 @@ const BaseContainer = withTracker(() => {
if (meetingEnded) Session.set('codeError', '410');
}
const approved = Users.findOne({ userId: Auth.userID, approved: true, guest: true });
const approved = !!Users.findOne({ userId: Auth.userID, approved: true, guest: true });
const ejected = Users.findOne({ userId: Auth.userID, ejected: true });
if (Session.get('codeError')) {
return {
User,
meetingHasEnded: !!meeting && meeting.meetingEnded,
approved,
ejected,
meetingIsBreakout: AppService.meetingIsBreakout(),
};
}
let userSubscriptionHandler;

View File

@ -17,6 +17,7 @@ import fa from 'react-intl/locale-data/fa';
import fr from 'react-intl/locale-data/fr';
import he from 'react-intl/locale-data/he';
import hi from 'react-intl/locale-data/hi';
import hu from 'react-intl/locale-data/hu';
import id from 'react-intl/locale-data/id';
import it from 'react-intl/locale-data/it';
import ja from 'react-intl/locale-data/ja';
@ -44,6 +45,7 @@ addLocaleData([
...fr,
...he,
...hi,
...hu,
...id,
...it,
...ja,

View File

@ -32,6 +32,7 @@ class ActionsBar extends React.PureComponent {
isSharingVideo,
screenShareEndAlert,
stopExternalVideoShare,
screenshareDataSavingSetting,
isCaptionsAvailable,
} = this.props;
@ -94,6 +95,7 @@ class ActionsBar extends React.PureComponent {
isUserPresenter,
screenSharingCheck,
screenShareEndAlert,
screenshareDataSavingSetting,
}}
/>
{isCaptionsAvailable

View File

@ -11,7 +11,7 @@ import VideoService from '../video-provider/service';
import ExternalVideoService from '/imports/ui/components/external-video-player/service';
import CaptionsService from '/imports/ui/components/captions/service';
import {
shareScreen, unshareScreen, isVideoBroadcasting, screenShareEndAlert,
shareScreen, unshareScreen, isVideoBroadcasting, screenShareEndAlert, dataSavingSetting,
} from '../screenshare/service';
import MediaService, { getSwapLayout } from '../media/service';
@ -51,6 +51,7 @@ export default withTracker(() => {
parseCurrentSlideContent: PresentationService.parseCurrentSlideContent,
isSharingVideo: Service.isSharingVideo(),
screenShareEndAlert,
screenshareDataSavingSetting: dataSavingSetting(),
isCaptionsAvailable: CaptionsService.isCaptionsAvailable(),
};
})(injectIntl(ActionsBarContainer));

View File

@ -15,6 +15,8 @@ const propTypes = {
handleUnshareScreen: PropTypes.func.isRequired,
isVideoBroadcasting: PropTypes.bool.isRequired,
screenSharingCheck: PropTypes.bool.isRequired,
screenShareEndAlert: PropTypes.func.isRequired,
screenshareDataSavingSetting: PropTypes.bool.isRequired,
};
const intlMessages = defineMessages({
@ -22,6 +24,10 @@ const intlMessages = defineMessages({
id: 'app.actionsBar.actionsDropdown.desktopShareLabel',
description: 'Desktop Share option label',
},
lockedDesktopShareLabel: {
id: 'app.actionsBar.actionsDropdown.lockedDesktopShareLabel',
description: 'Desktop locked Share option label',
},
stopDesktopShareLabel: {
id: 'app.actionsBar.actionsDropdown.stopDesktopShareLabel',
description: 'Stop Desktop Share option label',
@ -56,6 +62,7 @@ const DesktopShare = ({
isUserPresenter,
screenSharingCheck,
screenShareEndAlert,
screenshareDataSavingSetting,
}) => {
const onFail = (error) => {
switch (error) {
@ -69,15 +76,23 @@ const DesktopShare = ({
}
screenShareEndAlert();
};
const screenshareLocked = screenshareDataSavingSetting
? intlMessages.desktopShareLabel : intlMessages.lockedDesktopShareLabel;
const vLabel = isVideoBroadcasting
? intlMessages.stopDesktopShareLabel : screenshareLocked;
const vDescr = isVideoBroadcasting
? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc;
return (screenSharingCheck && !isMobileBrowser && isUserPresenter
? (
<Button
className={cx(styles.button, isVideoBroadcasting || styles.btn)}
icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'}
label={intl.formatMessage(isVideoBroadcasting
? intlMessages.stopDesktopShareLabel : intlMessages.desktopShareLabel)}
description={intl.formatMessage(isVideoBroadcasting
? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc)}
label={intl.formatMessage(vLabel)}
description={intl.formatMessage(vDescr)}
color={isVideoBroadcasting ? 'primary' : 'default'}
ghost={!isVideoBroadcasting}
hideLabel
@ -85,6 +100,7 @@ const DesktopShare = ({
size="lg"
onClick={isVideoBroadcasting ? handleUnshareScreen : () => handleShareScreen(onFail)}
id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'}
disabled={!screenshareDataSavingSetting}
/>
)
: null);

View File

@ -9,6 +9,7 @@ import PollingContainer from '/imports/ui/components/polling/container';
import logger from '/imports/startup/client/logger';
import ActivityCheckContainer from '/imports/ui/components/activity-check/container';
import UserInfoContainer from '/imports/ui/components/user-info/container';
import BreakoutRoomInvitation from '/imports/ui/components/breakout-room/invitation/container';
import ToastContainer from '../toast/container';
import ModalContainer from '../modal/container';
import NotificationsBarContainer from '../notifications-bar/container';
@ -48,6 +49,22 @@ const intlMessages = defineMessages({
id: 'app.iOSWarning.label',
description: 'message indicating to upgrade ios version',
},
clearedEmoji: {
id: 'app.toast.clearedEmoji.label',
description: 'message for cleared emoji status',
},
setEmoji: {
id: 'app.toast.setEmoji.label',
description: 'message when a user emoji has been set',
},
meetingMuteOn: {
id: 'app.toast.meetingMuteOn.label',
description: 'message used when meeting has been muted',
},
meetingMuteOff: {
id: 'app.toast.meetingMuteOff.label',
description: 'message used when meeting has been unmuted',
},
pollPublishedLabel: {
id: 'app.whiteboard.annotations.poll',
description: 'message displayed when a poll is published',
@ -110,9 +127,7 @@ class App extends Component {
if (!validIOSVersion()) {
notify(
intl.formatMessage(intlMessages.iOSWarning),
'error',
'warning',
intl.formatMessage(intlMessages.iOSWarning), 'error', 'warning',
);
}
@ -132,13 +147,38 @@ class App extends Component {
logger.info({ logCode: 'app_component_componentdidmount' }, 'Client loaded successfully');
}
componentDidUpdate(prevProps, prevState) {
const { hasPublishedPoll, intl, notify } = this.props;
componentDidUpdate(prevProps) {
const {
meetingMuted, notify, currentUserEmoji, intl, hasPublishedPoll,
} = this.props;
if (prevProps.currentUserEmoji.status !== currentUserEmoji.status) {
const formattedEmojiStatus = intl.formatMessage({ id: `app.actionsBar.emojiMenu.${currentUserEmoji.status}Label` })
|| currentUserEmoji.status;
notify(
currentUserEmoji.status === 'none'
? intl.formatMessage(intlMessages.clearedEmoji)
: intl.formatMessage(intlMessages.setEmoji, ({ 0: formattedEmojiStatus })),
'info',
currentUserEmoji.status === 'none'
? 'clear_status'
: 'user',
);
}
if (!prevProps.meetingMuted && meetingMuted) {
notify(
intl.formatMessage(intlMessages.meetingMuteOn), 'info', 'mute',
);
}
if (prevProps.meetingMuted && !meetingMuted) {
notify(
intl.formatMessage(intlMessages.meetingMuteOff), 'info', 'unmute',
);
}
if (!prevProps.hasPublishedPoll && hasPublishedPoll) {
notify(
intl.formatMessage(intlMessages.pollPublishedLabel),
'info',
'polling',
intl.formatMessage(intlMessages.pollPublishedLabel), 'info', 'polling',
);
}
}
@ -296,6 +336,7 @@ class App extends Component {
{this.renderPanel()}
{this.renderSidebar()}
</section>
<BreakoutRoomInvitation />
<PollingContainer />
<ModalContainer />
<AudioContainer />

View File

@ -11,6 +11,7 @@ import CaptionsService from '/imports/ui/components/captions/service';
import getFromUserSettings from '/imports/ui/services/users-settings';
import deviceInfo from '/imports/utils/deviceInfo';
import UserInfos from '/imports/api/users-infos';
import mapUser from '../../services/user/mapUser';
import {
getFontSize,
@ -69,6 +70,8 @@ const AppContainer = (props) => {
export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) => {
const currentUser = Users.findOne({ userId: Auth.userID });
const currentMeeting = Meetings.findOne({ meetingId: Auth.meetingID });
const { publishedPoll, voiceProp } = currentMeeting;
if (!currentUser.approved) {
baseControls.updateLoadingState(intl.formatMessage(intlMessages.waitingApprovalMessage));
@ -101,7 +104,9 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
notify,
validIOSVersion,
isPhone: deviceInfo.type().isPhone,
hasPublishedPoll: Meetings.findOne({ meetingId: Auth.meetingID }).publishedPoll,
meetingMuted: voiceProp.muteOnStart,
currentUserEmoji: mapUser(currentUser).emoji,
hasPublishedPoll: publishedPoll,
};
})(AppContainer)));

View File

@ -3,6 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/modal/service';
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 AudioControls from './component';
import AudioModalContainer from '../audio-modal/container';
import Service from '../service';
@ -31,9 +32,9 @@ const processToggleMuteFromOutside = (e) => {
}
};
export default withModalMounter(withTracker(({ mountModal }) => ({
export default lockContextContainer(withModalMounter(withTracker(({ mountModal, userLocks }) => ({
processToggleMuteFromOutside: arg => processToggleMuteFromOutside(arg),
showMute: Service.isConnected() && !Service.isListenOnly() && !Service.isEchoTest() && !Service.audioLocked(),
showMute: Service.isConnected() && !Service.isListenOnly() && !Service.isEchoTest() && !userLocks.userMic,
muted: Service.isConnected() && !Service.isListenOnly() && Service.isMuted(),
inAudio: Service.isConnected() && !Service.isEchoTest(),
listenOnly: Service.isConnected() && Service.isListenOnly(),
@ -43,4 +44,4 @@ export default withModalMounter(withTracker(({ mountModal }) => ({
handleToggleMuteMicrophone: () => Service.toggleMuteMicrophone(),
handleJoinAudio: () => (Service.isConnected() ? Service.joinListenOnly() : mountModal(<AudioModalContainer />)),
handleLeaveAudio: () => Service.exitAudio(),
}))(AudioControlsContainer));
}))(AudioControlsContainer)));

View File

@ -442,7 +442,7 @@ class AudioModal extends Component {
return (
<span>
{showPermissionsOvelay ? <PermissionsOverlay /> : null}
{showPermissionsOvelay ? <PermissionsOverlay closeModal={closeModal} /> : null}
<Modal
overlayClassName={styles.overlay}
className={styles.modal}

View File

@ -6,6 +6,7 @@ import getFromUserSettings from '/imports/ui/services/users-settings';
import AudioModal from './component';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
import Service from '../service';
const AudioModalContainer = props => <AudioModal {...props} />;
@ -13,7 +14,7 @@ const AudioModalContainer = props => <AudioModal {...props} />;
const APP_CONFIG = Meteor.settings.public.app;
export default withModalMounter(withTracker(({ mountModal }) => {
export default lockContextContainer(withModalMounter(withTracker(({ mountModal, userLocks }) => {
const listenOnlyMode = getFromUserSettings('listenOnlyMode', APP_CONFIG.listenOnlyMode);
const forceListenOnly = getFromUserSettings('forceListenOnly', APP_CONFIG.forceListenOnly);
const skipCheck = getFromUserSettings('skipCheck', APP_CONFIG.skipCheck);
@ -75,7 +76,7 @@ export default withModalMounter(withTracker(({ mountModal }) => {
formattedDialNum,
formattedTelVoice,
combinedDialInNum,
audioLocked: Service.audioLocked(),
audioLocked: userLocks.userMic,
joinFullAudioImmediately: !listenOnlyMode && skipCheck,
joinFullAudioEchoTest: !listenOnlyMode && !skipCheck,
forceListenOnlyAttendee: listenOnlyMode && forceListenOnly && !Service.isUserModerator(),
@ -83,4 +84,4 @@ export default withModalMounter(withTracker(({ mountModal }) => {
isMobileNative: navigator.userAgent.toLowerCase().includes('bbbnative'),
isIEOrEdge: browser().name === 'edge' || browser().name === 'ie',
});
})(AudioModalContainer));
})(AudioModalContainer)));

View File

@ -8,6 +8,7 @@ import Breakouts from '/imports/api/breakouts';
import { notify } from '/imports/ui/services/notification';
import getFromUserSettings from '/imports/ui/services/users-settings';
import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
import Service from './service';
import AudioModalContainer from './audio-modal/container';
@ -73,19 +74,20 @@ class AudioContainer extends React.Component {
let didMountAutoJoin = false;
export default withModalMounter(injectIntl(withTracker(({ mountModal, intl }) => {
export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ mountModal, intl, userLocks }) => {
const APP_CONFIG = Meteor.settings.public.app;
const KURENTO_CONFIG = Meteor.settings.public.kurento;
const autoJoin = getFromUserSettings('autoJoin', APP_CONFIG.autoJoin);
const { userWebcam, userMic } = userLocks;
const openAudioModal = () => new Promise((resolve) => {
mountModal(<AudioModalContainer resolve={resolve} />);
});
const openVideoPreviewModal = () => new Promise((resolve) => {
if (userWebcam) return resolve();
mountModal(<VideoPreviewContainer resolve={resolve} />);
});
if (Service.audioLocked()
if (userMic
&& Service.isConnected()
&& !Service.isListenOnly()
&& !Service.isMuted()) {
@ -144,4 +146,4 @@ export default withModalMounter(injectIntl(withTracker(({ mountModal, intl }) =>
}
},
};
})(AudioContainer)));
})(AudioContainer))));

View File

@ -1,10 +1,12 @@
import React from 'react';
import { injectIntl, intlShape, defineMessages } from 'react-intl';
import Modal from '/imports/ui/components/modal/simple/component';
import PropTypes from 'prop-types';
import { styles } from './styles';
const propTypes = {
intl: intlShape.isRequired,
closeModal: PropTypes.func.isRequired,
};
const intlMessages = defineMessages({

View File

@ -30,17 +30,6 @@ const init = (messages, intl) => {
AudioManager.init(userData);
};
const audioLocked = () => {
const userId = Auth.userID;
const User = mapUser(Users.findOne({ userId }));
const Meeting = Meetings.findOne({ meetingId: Auth.meetingID });
const lockSetting = Meeting.lockSettingsProps;
const audioLock = lockSetting ? lockSetting.disableMic : false;
return audioLock && User.isLocked;
};
const currentUser = () => mapUser(Users.findOne({ intId: Auth.userID }));
export default {
@ -66,6 +55,5 @@ export default {
isEchoTest: () => AudioManager.isEchoTest,
error: () => AudioManager.error,
isUserModerator: () => Users.findOne({ userId: Auth.userID }).moderator,
audioLocked,
currentUser,
};

View File

@ -3,7 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data';
import Breakouts from '/imports/api/breakouts';
import Auth from '/imports/ui/services/auth';
import { makeCall } from '/imports/ui/services/api';
import navBarService from '/imports/ui/components/nav-bar/service';
import breakoutService from '/imports/ui/components/breakout-room/service';
import BreakoutJoinConfirmationComponent from './component';
const BreakoutJoinConfirmationContrainer = props =>
@ -36,7 +36,7 @@ export default withTracker(({ breakout, mountModal, breakoutName }) => {
mountModal,
breakoutName,
breakoutURL: url,
breakouts: navBarService.getBreakouts(),
breakouts: breakoutService.getBreakouts(),
requestJoinURL,
getURL,
};

View File

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import _ from 'lodash';
import Button from '/imports/ui/components/button/component';
import { Session } from 'meteor/session';
import { styles } from './styles';
import BreakoutRoomContainer from './breakout-remaining-time/container';
@ -92,6 +93,7 @@ class BreakoutRoom extends Component {
}
getBreakoutURL(breakoutId) {
Session.set('lastBreakoutOpened', breakoutId);
const { requestJoinURL, breakoutRoomUser } = this.props;
const { waiting } = this.state;
const hasUser = breakoutRoomUser(breakoutId);

View File

@ -0,0 +1,57 @@
import React, { Component } from 'react';
import { Session } from 'meteor/session';
import { withModalMounter } from '/imports/ui/components/modal/service';
import BreakoutJoinConfirmation from '/imports/ui/components/breakout-join-confirmation/container';
const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mountModal(
<BreakoutJoinConfirmation
breakout={breakout}
breakoutName={breakoutName}
/>,
);
const closeBreakoutJoinConfirmation = mountModal => mountModal(null);
class BreakoutRoomInvitation extends Component {
componentDidUpdate(oldProps) {
const {
breakouts,
mountModal,
currentBreakoutUser,
getBreakoutByUser,
} = this.props;
const hadBreakouts = oldProps.breakouts.length;
const hasBreakouts = breakouts.length;
if (!hasBreakouts && hadBreakouts) {
closeBreakoutJoinConfirmation(mountModal);
}
if (hasBreakouts && currentBreakoutUser) {
const currentIsertedTime = currentBreakoutUser.insertedTime;
const oldCurrentUser = oldProps.currentBreakoutUser || {};
const oldInsertedTime = oldCurrentUser.insertedTime;
if (currentIsertedTime !== oldInsertedTime) {
const breakoutRoom = getBreakoutByUser(currentBreakoutUser);
const breakoutId = Session.get('lastBreakoutOpened');
if (breakoutRoom.breakoutId !== breakoutId) {
this.inviteUserToBreakout(breakoutRoom);
}
}
}
}
inviteUserToBreakout(breakout) {
const {
mountModal,
} = this.props;
openBreakoutJoinConfirmation.call(this, breakout, breakout.name, mountModal);
}
render() {
return null;
}
}
export default withModalMounter(BreakoutRoomInvitation);

View File

@ -0,0 +1,18 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import BreakoutRoomInvitation from './component';
import BreakoutService from '../service';
import Auth from '/imports/ui/services/auth';
const BreakoutRoomInvitationContainer = props => (
<BreakoutRoomInvitation {...props} />
);
export default withTracker(() => {
return {
breakouts: BreakoutService.getBreakouts(),
getBreakoutByUser: BreakoutService.getBreakoutByUser,
currentBreakoutUser: BreakoutService.getBreakoutUserByUserId(Auth.userID),
getBreakoutByUserId: BreakoutService.getBreakoutByUserId,
};
})(BreakoutRoomInvitationContainer);

View File

@ -5,6 +5,7 @@ import Auth from '/imports/ui/services/auth';
import { Session } from 'meteor/session';
import Users from '/imports/api/users';
import mapUser from '/imports/ui/services/user/mapUser';
import fp from 'lodash/fp';
const findBreakouts = () => {
const BreakoutRooms = Breakouts.find({
@ -70,6 +71,28 @@ const getUsersByBreakoutId = breakoutId => Users.find({
connectionStatus: 'online',
});
const getBreakoutByUserId = userId => Breakouts.find({ 'users.userId': userId }).fetch();
const getBreakoutByUser = user => Breakouts.findOne({ users: user });
const getUsersFromBreakouts = breakoutsArray => breakoutsArray
.map(breakout => breakout.users)
.reduce((acc, usersArray) => [...acc, ...usersArray], []);
const filterUserURLs = userId => breakoutUsersArray => breakoutUsersArray
.filter(user => user.userId === userId);
const getLastURLInserted = breakoutURLArray => breakoutURLArray
.sort((a, b) => a.insertedTime - b.insertedTime).pop();
const getBreakoutUserByUserId = userId => fp.pipe(
getBreakoutByUserId,
getUsersFromBreakouts,
filterUserURLs(userId),
getLastURLInserted,
)(userId);
const getBreakouts = () => Breakouts.find({}, { sort: { sequence: 1 } }).fetch();
export default {
findBreakouts,
@ -83,4 +106,8 @@ export default {
closeBreakoutPanel,
isModerator,
getUsersByBreakoutId,
getBreakoutUserByUserId,
getBreakoutByUser,
getBreakouts,
getBreakoutByUserId,
};

View File

@ -23,10 +23,22 @@ const intlMessages = defineMessages({
id: 'app.captions.menu.start',
description: 'Write closed captions',
},
ariaStart: {
id: 'app.captions.menu.ariaStart',
description: 'aria label for start captions button',
},
ariaStartDesc: {
id: 'app.captions.menu.ariaStartDesc',
description: 'aria description for start captions button',
},
select: {
id: 'app.captions.menu.select',
description: 'Select closed captions available language',
},
ariaSelect: {
id: 'app.captions.menu.ariaSelect',
description: 'Aria label for captions language selector',
},
});
const propTypes = {
@ -85,7 +97,13 @@ class WriterMenu extends PureComponent {
</h3>
</header>
<div className={styles.content}>
<label
aria-hidden
htmlFor="captionsLangSelector"
aria-label={intl.formatMessage(intlMessages.ariaSelect)}
/>
<select
id="captionsLangSelector"
className={styles.select}
onChange={this.handleChange}
defaultValue={DEFAULT_VALUE}
@ -102,9 +120,12 @@ class WriterMenu extends PureComponent {
<Button
className={styles.startBtn}
label={intl.formatMessage(intlMessages.start)}
aria-label={intl.formatMessage(intlMessages.ariaStart)}
aria-describedby="descriptionStart"
onClick={this.handleStart}
disabled={locale == null}
/>
<div id="descriptionStart" hidden>{intl.formatMessage(intlMessages.ariaStartDesc)}</div>
</div>
</Modal>
);

View File

@ -49,6 +49,7 @@ class EndMeetingComponent extends React.PureComponent {
</div>
<div className={styles.footer}>
<Button
data-test="confirmEndMeeting"
color="primary"
className={styles.button}
label={intl.formatMessage(intlMessages.yesLabel)}

View File

@ -2,9 +2,12 @@ import React, { Component } from 'react';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import YouTube from 'react-youtube';
import { sendMessage, onMessage } from './service';
import logger from '/imports/startup/client/logger';
const { PlayerState } = YouTube;
const SYNC_INTERVAL_SECONDS = 2;
class VideoPlayer extends Component {
constructor(props) {
super(props);
@ -102,7 +105,7 @@ class VideoPlayer extends Component {
const curTime = this.player.getCurrentTime();
const rate = this.player.getPlaybackRate();
sendMessage('playerUpdate', { rate, time: curTime, state: this.playerState });
}, 2000);
}, SYNC_INTERVAL_SECONDS * 1000);
} else {
onMessage('play', ({ time }) => {
this.presenterCommand = true;
@ -111,6 +114,7 @@ class VideoPlayer extends Component {
this.playerState = PlayerState.PLAYING;
this.player.playVideo();
}
logger.debug({ logCode: 'external_video_client_play' }, 'Play external video');
});
onMessage('stop', ({ time }) => {
@ -121,6 +125,7 @@ class VideoPlayer extends Component {
this.player.seekTo(time, true);
this.player.pauseVideo();
}
logger.debug({ logCode: 'external_video_client_stop' }, 'Stop external video');
});
onMessage('playerUpdate', (data) => {
@ -130,10 +135,12 @@ class VideoPlayer extends Component {
if (data.rate !== this.player.getPlaybackRate()) {
this.player.setPlaybackRate(data.rate);
logger.debug({ logCode: 'external_video_client_update_rate' }, 'Change external video playback rate to:', data.rate);
}
if (Math.abs(this.player.getCurrentTime() - data.time) > 2) {
if (Math.abs(this.player.getCurrentTime() - data.time) > SYNC_INTERVAL_SECONDS) {
this.player.seekTo(data.time, true);
logger.debug({ logCode: 'external_video_client_update_seek' }, 'Seek external video to:', data.time);
}
if (this.playerState !== data.state) {
@ -141,15 +148,13 @@ class VideoPlayer extends Component {
this.playerState = data.state;
if (this.playerState === PlayerState.PLAYING) {
this.player.playVideo();
logger.debug({ logCode: 'external_video_client_prevent_pause' }, 'Prevent pause external video');
} else {
this.player.pauseVideo();
logger.debug({ logCode: 'external_video_client_prevent_play' }, 'Prevent play external video');
}
}
});
onMessage('changePlaybackRate', (rate) => {
this.player.setPlaybackRate(rate);
});
}
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import lockContext from './context';
const contextConsumer = Component => props => (
<lockContext.Consumer>
{ contexts => <Component {...props} {...contexts} />}
</lockContext.Consumer>
);
export default contextConsumer;

View File

@ -0,0 +1,31 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import mapUser from '/imports/ui/services/user/mapUser';
import { LockStruct } from './context';
import { withLockContext } from './withContext';
const lockContextContainer = component => withTracker(() => {
const lockSetting = new LockStruct();
const Meeting = Meetings.findOne({ meetingId: Auth.meetingID });
const User = Users.findOne({ userId: Auth.userID });
const mappedUser = mapUser(User);
const userIsLocked = mappedUser.isLocked;
const lockSettings = Meeting.lockSettingsProps;
lockSetting.isLocked = userIsLocked;
lockSetting.lockSettings = lockSettings;
lockSetting.userLocks.userWebcam = userIsLocked && lockSettings.disableCam;
lockSetting.userLocks.userMic = userIsLocked && lockSettings.disableMic;
lockSetting.userLocks.userNote = userIsLocked && lockSettings.disableNote;
lockSetting.userLocks.userPrivateChat = userIsLocked && lockSettings.disablePrivateChat;
lockSetting.userLocks.userPublicChat = userIsLocked && lockSettings.disablePublicChat;
lockSetting.userLocks.userLockedLayout = userIsLocked && lockSettings.lockedLayout;
return lockSetting;
})(withLockContext(component));
export default lockContextContainer;

View File

@ -0,0 +1,29 @@
import React from 'react';
export function LockStruct() {
return ({
isLocked: false,
lockSettings: {
disableCam: false,
disableMic: false,
disableNote: false,
disablePrivateChat: false,
disablePublicChat: false,
lockOnJoin: true,
lockOnJoinConfigurable: false,
lockedLayout: false,
},
userLocks: {
userWebcam: false,
userMic: false,
userNote: false,
userPrivateChat: false,
userPublicChat: false,
userLockedLayout: false,
},
});
}
const lockContext = React.createContext(new LockStruct());
export default lockContext;

View File

@ -0,0 +1,11 @@
import React from 'react';
import lockContext from './context';
const contextProvider = props => (
<lockContext.Provider value={props}>
{ props.children }
</lockContext.Provider>
);
export default contextProvider;

View File

@ -0,0 +1,19 @@
import React from 'react';
import LockProvider from './provider';
import LockConsumer from './consumer';
const withProvider = Component => props => (
<LockProvider {...props}>
<Component />
</LockProvider>
);
const withConsumer = Component => LockConsumer(Component);
const withLockContext = Component => withProvider(withConsumer(Component));
export {
withProvider,
withConsumer,
withLockContext,
};

View File

@ -3,7 +3,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import { notify } from '/imports/ui/services/notification';
import _ from 'lodash';
const intlMessages = defineMessages({
const intlDisableMessages = defineMessages({
disableCam: {
id: 'app.userList.userOptions.disableCam',
description: 'label to disable cam notification',
@ -30,6 +30,33 @@ const intlMessages = defineMessages({
},
});
const intlEnableMessages = defineMessages({
disableCam: {
id: 'app.userList.userOptions.enableCam',
description: 'label to enable cam notification',
},
disableMic: {
id: 'app.userList.userOptions.enableMic',
description: 'label to enable mic notification',
},
disablePrivateChat: {
id: 'app.userList.userOptions.enablePrivChat',
description: 'label to enable private chat notification',
},
disablePublicChat: {
id: 'app.userList.userOptions.enablePubChat',
description: 'label to enable private chat notification',
},
disableNote: {
id: 'app.userList.userOptions.enableNote',
description: 'label to enable note notification',
},
onlyModeratorWebcam: {
id: 'app.userList.userOptions.enableOnlyModeratorWebcam',
description: 'label to enable all webcams except for the moderators cam',
},
});
class LockViewersNotifyComponent extends Component {
componentDidUpdate(prevProps) {
const {
@ -43,18 +70,36 @@ class LockViewersNotifyComponent extends Component {
webcamsOnlyForModerator: prevWebcamsOnlyForModerator,
} = prevProps;
if (!_.isEqual(lockSettings, prevLockSettings)) {
const rejectedKeys = ['setBy', 'lockedLayout'];
const filteredSettings = Object.keys(lockSettings)
.filter(key => prevLockSettings[key] !== lockSettings[key]
&& lockSettings[key]
&& !rejectedKeys.includes(key));
filteredSettings.forEach((key) => {
function notifyLocks(arrLocks, intlMessages) {
arrLocks.forEach((key) => {
notify(intl.formatMessage(intlMessages[key]), 'info', 'lock');
});
}
if (!_.isEqual(lockSettings, prevLockSettings)) {
const rejectedKeys = ['setBy', 'lockedLayout'];
const disabledSettings = Object.keys(lockSettings)
.filter(key => prevLockSettings[key] !== lockSettings[key]
&& lockSettings[key]
&& !rejectedKeys.includes(key));
const enableSettings = Object.keys(lockSettings)
.filter(key => prevLockSettings[key] !== lockSettings[key]
&& !lockSettings[key]
&& !rejectedKeys.includes(key));
if (disabledSettings.length > 0) {
notifyLocks(disabledSettings, intlDisableMessages);
}
if (enableSettings.length > 0) {
notifyLocks(enableSettings, intlEnableMessages);
}
}
if (webcamsOnlyForModerator && !prevWebcamsOnlyForModerator) {
notify(intl.formatMessage(intlMessages.onlyModeratorWebcam), 'info', 'lock');
notify(intl.formatMessage(intlDisableMessages.onlyModeratorWebcam), 'info', 'lock');
}
if (!webcamsOnlyForModerator && prevWebcamsOnlyForModerator) {
notify(intl.formatMessage(intlEnableMessages.onlyModeratorWebcam), 'info', 'lock');
}
}

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import VideoProviderContainer from '/imports/ui/components/video-provider/container';
import WebcamDraggableOverlay from './webcam-draggable-overlay/component';
import { styles } from './styles';
@ -18,6 +18,11 @@ const defaultProps = {
export default class Media extends Component {
constructor(props) {
super(props);
this.refContainer = React.createRef();
}
componentWillUpdate() {
window.dispatchEvent(new Event('resize'));
}
@ -60,13 +65,22 @@ export default class Media extends Component {
});
return (
<div className={styles.container}>
<div
id="container"
className={cx(styles.container)}
ref={this.refContainer}
>
<div className={!swapLayout ? contentClassName : overlayClassName}>
{children}
</div>
<div className={!swapLayout ? overlayClassName : contentClassName}>
{ !disableVideo && !audioModalIsOpen ? <VideoProviderContainer /> : null }
</div>
<WebcamDraggableOverlay
refMediaContainer={this.refContainer}
swapLayout={swapLayout}
floatingOverlay={floatingOverlay}
hideOverlay={hideOverlay}
disableVideo={disableVideo}
audioModalIsOpen={audioModalIsOpen}
/>
</div>
);
}

View File

@ -1,4 +1,5 @@
@import "../../stylesheets/variables/_all";
@import "../../stylesheets/variables/video";
.container {
order: 1;
@ -18,20 +19,40 @@
overflow: auto;
width: 100%;
position: relative;
order: 2;
}
.overlay {
flex: 1;
display: flex;
order: 1;
width: 100%;
border: 5px solid transparent;
border-top: 0 !important;
position: relative;
border: 0;
margin-top: 10px;
margin-bottom: 10px;
z-index: 2;
align-items: center;
min-height: calc(var(--video-width) / var(--video-ratio));
}
@include mq($medium-up) {
border: 10px solid transparent;
}
.overlayRelative{
position: relative;
}
.overlayAbsoluteSingle{
position: absolute;
height: calc(var(--video-width) / var(--video-ratio));
}
.overlayAbsoluteMult{
position: absolute;
}
.overlayToTop {
order: 3;
}
.overlayToBottom {
order: 1;
}
.hideOverlay {
@ -44,28 +65,60 @@
}
.floatingOverlay {
--overlay-width: 20vw;
--overlay-min-width: 235px;
--overlay-max-width: 20vw;
--overlay-ratio: calc(16 / 9);
margin: 10px;
@include mq($medium-up) {
z-index: 999;
position: fixed;
margin: 0;
bottom: .8rem;
left: auto;
right: .8rem;
width: var(--overlay-width);
min-width: var(--overlay-min-width);
max-width: var(--overlay-max-width);
height: calc(var(--overlay-width) / var(--overlay-ratio));
min-height: calc(var(--overlay-min-width) / var(--overlay-ratio));
max-height: calc(var(--overlay-max-width) / var(--overlay-ratio));
[dir="rtl"] & {
right: auto;
left: .8rem;
}
position: absolute;
bottom: 0;
right: 0;
width: var(--video-width);
min-width: var(--video-width);
max-width: var(--video-max-width);
height: calc(var(--video-width) / var(--video-ratio));
min-height: calc(var(--video-width) / var(--video-ratio));
}
}
.hide {
display: none;
}
.show {
display: block;
}
.top {
top: 0;
}
.bottom {
bottom: 0;
}
.dragging {
opacity: .5;
}
.dropZoneTop,
.dropZoneBottom {
border: 1px dashed var(--color-gray-light);
position: absolute;
width: 100%;
z-index: 9999;
}
.dropZoneTop {
top: 0;
}
.dropZoneBottom {
bottom: 0;
}
.dropZoneBg {
position: absolute;
z-index: 99;
width: 100%;
background-color: rgba(255, 255, 255, .3);
}

View File

@ -0,0 +1,594 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import VideoProviderContainer from '/imports/ui/components/video-provider/container';
import _ from 'lodash';
import browser from 'browser-detect';
import Draggable from 'react-draggable';
import { styles } from '../styles';
const propTypes = {
floatingOverlay: PropTypes.bool,
hideOverlay: PropTypes.bool,
};
const defaultProps = {
floatingOverlay: false,
hideOverlay: true,
};
const fullscreenChangedEvents = [
'fullscreenchange',
'webkitfullscreenchange',
'mozfullscreenchange',
'MSFullscreenChange',
];
const BROWSER_ISMOBILE = browser().mobile;
export default class WebcamDraggableOverlay extends Component {
static getWebcamBySelector() {
return document.querySelector('video[class^="media"]');
}
static getWebcamBySelectorCount() {
return document.querySelectorAll('video[class^="media"]').length;
}
static getWebcamListBySelector() {
return document.querySelector('div[class^="videoList"]');
}
static getVideoCanvasBySelector() {
return document.querySelector('div[class^="videoCanvas"]');
}
static getOverlayBySelector() {
return document.querySelector('div[class*="overlay"]');
}
static isOverlayAbsolute() {
return !!(document.querySelector('div[class*="overlayAbsolute"]'));
}
static getIsOverlayChanged() {
const overlayToTop = document.querySelector('div[class*="overlayToTop"]');
const overlayToBottom = document.querySelector('div[class*="overlayToBottom"]');
return !!(overlayToTop || overlayToBottom);
}
static getGridLineNum(numCams, camWidth, containerWidth) {
let used = (camWidth + 10) * numCams;
let countLines = 0;
while (used > containerWidth) {
used -= containerWidth;
countLines += 1;
}
return countLines + 1;
}
static waitFor(condition, callback) {
const cond = condition();
if (!cond) {
setTimeout(WebcamDraggableOverlay.waitFor.bind(null, condition, callback), 500);
} else {
callback();
}
return false;
}
constructor(props) {
super(props);
this.state = {
dragging: false,
showDropZones: false,
showBgDropZoneTop: false,
showBgDropZoneBottom: false,
dropOnTop: BROWSER_ISMOBILE,
dropOnBottom: !BROWSER_ISMOBILE,
initialPosition: { x: 0, y: 0 },
initialRectPosition: { x: 0, y: 0 },
lastPosition: { x: 0, y: 0 },
resetPosition: false,
isFullScreen: false,
isVideoLoaded: false,
isMinWidth: false,
userLength: 0,
shouldUpdatePosition: true,
};
this.updateWebcamPositionByResize = this.updateWebcamPositionByResize.bind(this);
this.eventVideoFocusChangeListener = this.eventVideoFocusChangeListener.bind(this);
this.eventResizeListener = _.throttle(
this.updateWebcamPositionByResize,
500,
{
leading: true,
trailing: true,
},
);
this.videoMounted = this.videoMounted.bind(this);
this.handleWebcamDragStart = this.handleWebcamDragStart.bind(this);
this.handleWebcamDragStop = this.handleWebcamDragStop.bind(this);
this.handleFullscreenChange = this.handleFullscreenChange.bind(this);
this.fullscreenButtonChange = this.fullscreenButtonChange.bind(this);
this.getVideoListUsersChange = this.getVideoListUsersChange.bind(this);
this.setIsFullScreen = this.setIsFullScreen.bind(this);
this.setResetPosition = this.setResetPosition.bind(this);
this.setInitialReferencePoint = this.setInitialReferencePoint.bind(this);
this.setLastPosition = this.setLastPosition.bind(this);
this.setShouldUpdatePosition = this.setShouldUpdatePosition.bind(this);
this.setLastWebcamPosition = this.setLastWebcamPosition.bind(this);
this.setisMinWidth = this.setisMinWidth.bind(this);
this.setDropOnBottom = this.setDropOnBottom.bind(this);
this.dropZoneTopEnterHandler = this.dropZoneTopEnterHandler.bind(this);
this.dropZoneTopLeaveHandler = this.dropZoneTopLeaveHandler.bind(this);
this.dropZoneBottomEnterHandler = this.dropZoneBottomEnterHandler.bind(this);
this.dropZoneBottomLeaveHandler = this.dropZoneBottomLeaveHandler.bind(this);
this.dropZoneTopMouseUpHandler = this.dropZoneTopMouseUpHandler.bind(this);
this.dropZoneBottomMouseUpHandler = this.dropZoneBottomMouseUpHandler.bind(this);
}
componentDidMount() {
const { floatingOverlay } = this.props;
const { resetPosition } = this.state;
if (!floatingOverlay
&& !resetPosition) this.setResetPosition(true);
window.addEventListener('resize', this.eventResizeListener);
window.addEventListener('videoFocusChange', this.eventVideoFocusChangeListener);
fullscreenChangedEvents.forEach((event) => {
document.addEventListener(event, this.handleFullscreenChange);
});
// Ensures that the event will be called before the resize
document.addEventListener('webcamFullscreenButtonChange', this.fullscreenButtonChange);
window.addEventListener('videoListUsersChange', this.getVideoListUsersChange);
}
componentDidUpdate(prevProps, prevState) {
const { swapLayout } = this.props;
const { userLength, lastPosition } = this.state;
const { y } = lastPosition;
// if (prevProps.swapLayout && !swapLayout && userLength === 1) {
// this.setShouldUpdatePosition(false);
// }
if (prevProps.swapLayout && !swapLayout && userLength > 1) {
this.setLastPosition(0, y);
}
if (prevState.userLength === 1 && userLength > 1) {
this.setDropOnBottom(true);
this.setResetPosition(true);
}
}
componentWillUnmount() {
fullscreenChangedEvents.forEach((event) => {
document.removeEventListener(event, this.fullScreenToggleCallback);
});
document.removeEventListener('webcamFullscreenButtonChange', this.fullscreenButtonChange);
document.removeEventListener('videoListUsersChange', this.getVideoListUsersChange);
document.removeEventListener('videoFocusChange', this.eventVideoFocusChangeListener);
}
getVideoListUsersChange() {
const userLength = WebcamDraggableOverlay.getWebcamBySelectorCount();
this.setState({ userLength });
}
setIsFullScreen(isFullScreen) {
this.setState({ isFullScreen });
}
setResetPosition(resetPosition) {
this.setState({ resetPosition });
}
setLastPosition(x, y) {
this.setState({ lastPosition: { x, y } });
}
setShouldUpdatePosition(shouldUpdatePosition) {
this.setState({ shouldUpdatePosition });
}
setDropOnBottom(dropOnBottom) {
this.setState({ dropOnBottom });
}
setInitialReferencePoint() {
const { refMediaContainer } = this.props;
const { userLength, shouldUpdatePosition } = this.state;
const { current: mediaContainer } = refMediaContainer;
const webcamBySelector = WebcamDraggableOverlay.getWebcamBySelector();
if (webcamBySelector && mediaContainer && shouldUpdatePosition) {
if (userLength === 0) this.getVideoListUsersChange();
let x = 0;
let y = 0;
const webcamBySelectorRect = webcamBySelector.getBoundingClientRect();
const {
width: webcamWidth,
height: webcamHeight,
} = webcamBySelectorRect;
const mediaContainerRect = mediaContainer.getBoundingClientRect();
const {
width: mediaWidth,
height: mediaHeight,
} = mediaContainerRect;
const lineNum = WebcamDraggableOverlay
.getGridLineNum(userLength, webcamWidth, mediaWidth);
x = mediaWidth - ((webcamWidth + 10) * userLength); // 10 is margin
y = mediaHeight - ((webcamHeight + 10) * lineNum); // 10 is margin
if (x === 0 && y === 0) return false;
this.setState({ initialRectPosition: { x, y } });
return true;
}
return false;
}
setLastWebcamPosition() {
const { refMediaContainer } = this.props;
const { current: mediaContainer } = refMediaContainer;
const { initialRectPosition, userLength, shouldUpdatePosition } = this.state;
const { x: initX, y: initY } = initialRectPosition;
const webcamBySelector = WebcamDraggableOverlay.getWebcamBySelector();
if (webcamBySelector && mediaContainer && shouldUpdatePosition) {
const webcamBySelectorRect = webcamBySelector.getBoundingClientRect();
const {
left: webcamLeft,
top: webcamTop,
} = webcamBySelectorRect;
const mediaContainerRect = mediaContainer.getBoundingClientRect();
const {
left: mediaLeft,
top: mediaTop,
} = mediaContainerRect;
const webcamXByMedia = userLength > 1 ? 0 : webcamLeft - mediaLeft;
const webcamYByMedia = webcamTop - mediaTop;
let x = 0;
let y = 0;
if (webcamXByMedia > 0) {
x = webcamXByMedia - initX;
} else {
x = 0 - initX;
}
if (userLength > 1) x = 0;
if (webcamYByMedia > 0) {
y = webcamYByMedia - initY;
} else {
y = 0 - initY;
}
if (webcamYByMedia > initY) {
y = -10;
}
this.setLastPosition(x, y);
}
}
setisMinWidth(isMinWidth) {
this.setState({ isMinWidth });
}
videoMounted() {
this.setResetPosition(true);
WebcamDraggableOverlay.waitFor(this.setInitialReferencePoint, this.setLastWebcamPosition);
this.setState({ isVideoLoaded: true });
}
fullscreenButtonChange() {
this.setIsFullScreen(true);
}
updateWebcamPositionByResize() {
const {
isVideoLoaded,
isMinWidth,
} = this.state;
if (isVideoLoaded) {
this.setInitialReferencePoint();
this.setLastWebcamPosition();
}
if (window.innerWidth < 641) {
this.setisMinWidth(true);
this.setState({ dropOnBottom: true });
this.setResetPosition(true);
} else if (isMinWidth) {
this.setisMinWidth(false);
}
}
eventVideoFocusChangeListener() {
setTimeout(() => {
this.setInitialReferencePoint();
this.setLastWebcamPosition();
}, 500);
}
handleFullscreenChange() {
if (document.fullscreenElement
|| document.webkitFullscreenElement
|| document.mozFullScreenElement
|| document.msFullscreenElement) {
window.removeEventListener('resize', this.eventResizeListener);
this.setIsFullScreen(true);
} else {
this.setIsFullScreen(false);
window.addEventListener('resize', this.eventResizeListener);
}
}
handleWebcamDragStart() {
const { floatingOverlay } = this.props;
const {
dragging,
showDropZones,
dropOnTop,
dropOnBottom,
resetPosition,
} = this.state;
if (!floatingOverlay) WebcamDraggableOverlay.getOverlayBySelector().style.bottom = 0;
if (!dragging) this.setState({ dragging: true });
if (!showDropZones) this.setState({ showDropZones: true });
if (dropOnTop) this.setState({ dropOnTop: false });
if (dropOnBottom) this.setState({ dropOnBottom: false });
if (resetPosition) this.setState({ resetPosition: false });
window.dispatchEvent(new Event('resize'));
}
handleWebcamDragStop(e, position) {
const {
dragging,
showDropZones,
} = this.state;
const { x, y } = position;
if (dragging) this.setState({ dragging: false });
if (showDropZones) this.setState({ showDropZones: false });
this.setLastPosition(x, y);
}
dropZoneTopEnterHandler() {
const {
showBgDropZoneTop,
} = this.state;
if (!showBgDropZoneTop) this.setState({ showBgDropZoneTop: true });
}
dropZoneBottomEnterHandler() {
const {
showBgDropZoneBottom,
} = this.state;
if (!showBgDropZoneBottom) this.setState({ showBgDropZoneBottom: true });
}
dropZoneTopLeaveHandler() {
const {
showBgDropZoneTop,
} = this.state;
if (showBgDropZoneTop) this.setState({ showBgDropZoneTop: false });
}
dropZoneBottomLeaveHandler() {
const {
showBgDropZoneBottom,
} = this.state;
if (showBgDropZoneBottom) this.setState({ showBgDropZoneBottom: false });
}
dropZoneTopMouseUpHandler() {
const { dropOnTop } = this.state;
if (!dropOnTop) {
this.setState({
dropOnTop: true,
dropOnBottom: false,
resetPosition: true,
});
}
window.dispatchEvent(new Event('resize'));
setTimeout(() => this.setLastWebcamPosition(), 500);
}
dropZoneBottomMouseUpHandler() {
const { dropOnBottom } = this.state;
if (!dropOnBottom) {
this.setState({
dropOnTop: false,
dropOnBottom: true,
resetPosition: true,
});
}
window.dispatchEvent(new Event('resize'));
setTimeout(() => this.setLastWebcamPosition(), 500);
}
render() {
const {
swapLayout,
floatingOverlay,
hideOverlay,
disableVideo,
audioModalIsOpen,
refMediaContainer,
} = this.props;
const { current: mediaContainer } = refMediaContainer;
let mediaContainerRect;
let mediaHeight;
if (mediaContainer) {
mediaContainerRect = mediaContainer.getBoundingClientRect();
const {
height,
} = mediaContainerRect;
mediaHeight = height;
}
const {
dragging,
showDropZones,
showBgDropZoneTop,
showBgDropZoneBottom,
dropOnTop,
dropOnBottom,
initialPosition,
lastPosition,
resetPosition,
isFullScreen,
isMinWidth,
} = this.state;
const webcamBySelectorCount = WebcamDraggableOverlay.getWebcamBySelectorCount();
const contentClassName = cx({
[styles.content]: true,
});
const overlayClassName = cx({
[styles.overlay]: true,
[styles.overlayRelative]: (dropOnTop || dropOnBottom),
[styles.overlayAbsoluteSingle]: (!dropOnTop && !dropOnBottom && webcamBySelectorCount <= 1),
[styles.overlayAbsoluteMult]: (!dropOnTop && !dropOnBottom && webcamBySelectorCount > 1),
[styles.hideOverlay]: hideOverlay,
[styles.floatingOverlay]: floatingOverlay && (!dropOnTop && !dropOnBottom),
[styles.overlayToTop]: dropOnTop,
[styles.overlayToBottom]: dropOnBottom,
[styles.dragging]: dragging,
});
const dropZoneTopClassName = cx({
[styles.dropZoneTop]: true,
[styles.show]: showDropZones,
[styles.hide]: !showDropZones,
});
const dropZoneBottomClassName = cx({
[styles.dropZoneBottom]: true,
[styles.show]: showDropZones,
[styles.hide]: !showDropZones,
});
const dropZoneBgTopClassName = cx({
[styles.dropZoneBg]: true,
[styles.top]: true,
[styles.show]: showBgDropZoneTop,
[styles.hide]: !showBgDropZoneTop,
});
const dropZoneBgBottomClassName = cx({
[styles.dropZoneBg]: true,
[styles.bottom]: true,
[styles.show]: showBgDropZoneBottom,
[styles.hide]: !showBgDropZoneBottom,
});
const cursor = () => {
if ((!swapLayout || !isFullScreen || !BROWSER_ISMOBILE || !isMinWidth) && !dragging) return 'grab';
if (dragging) return 'grabbing';
return 'default';
};
return (
<Fragment>
<div
className={dropZoneTopClassName}
onMouseEnter={this.dropZoneTopEnterHandler}
onMouseLeave={this.dropZoneTopLeaveHandler}
onMouseUp={this.dropZoneTopMouseUpHandler}
role="presentation"
style={{ height: '100px' }}
/>
<div
className={dropZoneBgTopClassName}
style={{ height: '100px' }}
/>
<Draggable
handle="video"
bounds="#container"
onStart={this.handleWebcamDragStart}
onStop={this.handleWebcamDragStop}
onMouseDown={e => e.preventDefault()}
disabled={swapLayout || isFullScreen || BROWSER_ISMOBILE || isMinWidth}
position={resetPosition || swapLayout ? initialPosition : lastPosition}
>
<div
className={!swapLayout ? overlayClassName : contentClassName}
>
{
!disableVideo && !audioModalIsOpen
? (
<VideoProviderContainer
cursor={cursor()}
swapLayout={swapLayout}
mediaHeight={mediaHeight}
onMount={this.videoMounted}
onUpdate={this.videoUpdated}
/>
) : null}
</div>
</Draggable>
<div
className={dropZoneBottomClassName}
onMouseEnter={this.dropZoneBottomEnterHandler}
onMouseLeave={this.dropZoneBottomLeaveHandler}
onMouseUp={this.dropZoneBottomMouseUpHandler}
role="presentation"
style={{ height: '100px' }}
/>
<div
className={dropZoneBgBottomClassName}
style={{ height: '100px' }}
/>
</Fragment>
);
}
}
WebcamDraggableOverlay.propTypes = propTypes;
WebcamDraggableOverlay.defaultProps = defaultProps;

View File

@ -170,7 +170,7 @@ class MeetingEnded extends React.PureComponent {
<div className={styles.parent}>
<div className={styles.modal}>
<div className={styles.content}>
<h1 className={styles.title}>
<h1 className={styles.title} data-test="meetingEndedModalTitle">
{
intl.formatMessage(intlMessage[code] || intlMessage[430])
}

View File

@ -40,6 +40,7 @@ const propTypes = {
disabled: PropTypes.bool,
}),
preventClosing: PropTypes.bool,
shouldCloseOnOverlayClick: PropTypes.bool,
};
const defaultProps = {
@ -54,9 +55,31 @@ const defaultProps = {
};
class ModalFullscreen extends PureComponent {
constructor(props) {
super(props);
this.handleAction = this.handleAction.bind(this);
}
handleAction(name) {
const action = this.props[name];
return this.props.modalHide(action.callback);
const { confirm, dismiss, modalHide } = this.props;
const { callback: callBackConfirm } = confirm;
const { callback: callBackDismiss } = dismiss;
let callback;
switch (name) {
case 'confirm':
callback = callBackConfirm;
break;
case 'dismiss':
callback = callBackDismiss;
break;
default:
break;
}
return modalHide(callback);
}
render() {
@ -66,6 +89,7 @@ class ModalFullscreen extends PureComponent {
confirm,
dismiss,
className,
children,
modalisOpen,
preventClosing,
...otherProps
@ -93,17 +117,17 @@ class ModalFullscreen extends PureComponent {
label={intl.formatMessage(intlMessages.modalClose)}
aria-label={`${intl.formatMessage(intlMessages.modalClose)} ${title}`}
disabled={dismiss.disabled}
onClick={this.handleAction.bind(this, 'dismiss')}
onClick={() => this.handleAction('dismiss')}
aria-describedby="modalDismissDescription"
/>
<Button
data-test="modalConfirmButton"
color="primary"
className={popoutIcon ? cx(styles.confirm, styles.popout) : styles.confirm}
label={confirm.label}
label={confirm.label || intl.formatMessage(intlMessages.modalDone)}
aria-label={confirmAriaLabel}
disabled={confirm.disabled}
onClick={this.handleAction.bind(this, 'confirm')}
onClick={() => this.handleAction('confirm')}
aria-describedby="modalConfirmDescription"
icon={confirm.icon || null}
iconRight={popoutIcon}
@ -111,7 +135,7 @@ class ModalFullscreen extends PureComponent {
</div>
</header>
<div className={styles.content}>
{this.props.children}
{children}
</div>
<div id="modalDismissDescription" hidden>{intl.formatMessage(intlMessages.modalCloseDescription)}</div>
<div id="modalConfirmDescription" hidden>{intl.formatMessage(intlMessages.modalDoneDescription)}</div>

View File

@ -3,14 +3,6 @@ import PropTypes from 'prop-types';
import { Session } from 'meteor/session';
import _ from 'lodash';
import cx from 'classnames';
import Auth from '/imports/ui/services/auth';
import Icon from '/imports/ui/components/icon/component';
import BreakoutJoinConfirmation from '/imports/ui/components/breakout-join-confirmation/container';
import Dropdown from '/imports/ui/components/dropdown/component';
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 { withModalMounter } from '/imports/ui/components/modal/service';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import getFromUserSettings from '/imports/ui/services/users-settings';
@ -81,15 +73,6 @@ const defaultProps = {
shortcuts: '',
};
const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mountModal(
<BreakoutJoinConfirmation
breakout={breakout}
breakoutName={breakoutName}
/>,
);
const closeBreakoutJoinConfirmation = mountModal => mountModal(null);
class NavBar extends PureComponent {
static handleToggleUserList() {
Session.set(
@ -105,8 +88,6 @@ class NavBar extends PureComponent {
super(props);
this.state = {
isActionsOpen: false,
didSendBreakoutInvite: false,
time: (props.recordProps.time ? props.recordProps.time : 0),
amIModerator: props.amIModerator,
};
@ -131,14 +112,9 @@ class NavBar extends PureComponent {
return { amIModerator: nextProps.amIModerator };
}
componentDidUpdate(oldProps) {
componentDidUpdate() {
const {
breakouts,
isBreakoutRoom,
mountModal,
recordProps,
currentBreakoutUser,
getBreakoutByUser,
} = this.props;
if (!recordProps.recording) {
@ -147,68 +123,12 @@ class NavBar extends PureComponent {
} else if (this.interval === null) {
this.interval = setInterval(this.incrementTime, 1000);
}
const {
didSendBreakoutInvite,
} = this.state;
const hadBreakouts = oldProps.breakouts.length;
const hasBreakouts = breakouts.length;
if (!hasBreakouts && hadBreakouts) {
closeBreakoutJoinConfirmation(mountModal);
}
if (hasBreakouts && currentBreakoutUser) {
const currentIsertedTime = currentBreakoutUser.insertedTime;
const oldCurrentUser = oldProps.currentBreakoutUser || {};
const oldInsertedTime = oldCurrentUser.insertedTime;
if (currentIsertedTime !== oldInsertedTime) {
const breakoutRoom = getBreakoutByUser(currentBreakoutUser);
this.inviteUserToBreakout(breakoutRoom);
}
}
breakouts.forEach((breakout) => {
const userOnMeeting = breakout.users.filter(u => u.userId === Auth.userID).length;
if (breakout.freeJoin
&& !didSendBreakoutInvite
&& !userOnMeeting
&& !isBreakoutRoom) {
this.inviteUserToBreakout(breakout);
this.setState({ didSendBreakoutInvite: true });
}
if (!breakout.users) {
return;
}
if (!userOnMeeting) return;
if ((!didSendBreakoutInvite && !isBreakoutRoom)) {
this.inviteUserToBreakout(breakout);
}
});
if (!breakouts.length && didSendBreakoutInvite) {
this.setState({ didSendBreakoutInvite: false });
}
}
componentWillUnmount() {
clearInterval(this.interval);
}
inviteUserToBreakout(breakout) {
const {
mountModal,
} = this.props;
this.setState({ didSendBreakoutInvite: true }, () => {
openBreakoutJoinConfirmation.call(this, breakout, breakout.name, mountModal);
});
}
incrementTime() {
const { recordProps } = this.props;
const { time } = this.state;
@ -220,62 +140,6 @@ class NavBar extends PureComponent {
}
}
renderPresentationTitle() {
const {
breakouts,
isBreakoutRoom,
presentationTitle,
} = this.props;
const {
isActionsOpen,
} = this.state;
if (isBreakoutRoom || !breakouts.length) {
return (
<h1 className={styles.presentationTitle}>{presentationTitle}</h1>
);
}
const breakoutItems = breakouts.map(breakout => this.renderBreakoutItem(breakout));
return (
<Dropdown isOpen={isActionsOpen}>
<DropdownTrigger>
<h1 className={cx(styles.presentationTitle, styles.dropdownBreakout)}>
{presentationTitle}
{' '}
<Icon iconName="down-arrow" />
</h1>
</DropdownTrigger>
<DropdownContent
placement="bottom"
>
<DropdownList>
{breakoutItems}
</DropdownList>
</DropdownContent>
</Dropdown>
);
}
renderBreakoutItem(breakout) {
const {
mountModal,
} = this.props;
const breakoutName = breakout.name;
return (
<DropdownListItem
key={_.uniqueId('action-header')}
label={breakoutName}
onClick={
openBreakoutJoinConfirmation.bind(this, breakout, breakoutName, mountModal)
}
/>
);
}
render() {
const {
hasUnreadMessages,
@ -285,12 +149,11 @@ class NavBar extends PureComponent {
shortcuts: TOGGLE_USERLIST_AK,
mountModal,
isBreakoutRoom,
presentationTitle,
} = this.props;
const recordingMessage = recordProps.recording ? 'recordingIndicatorOn' : 'recordingIndicatorOff';
const { time, amIModerator } = this.state;
let recordTitle;
if (!this.interval) {
@ -329,7 +192,7 @@ class NavBar extends PureComponent {
/>
</div>
<div className={styles.center}>
{this.renderPresentationTitle()}
<h1 className={styles.presentationTitle}>{presentationTitle}</h1>
{recordProps.record
? <span className={styles.presentationTitleSeparator} aria-hidden>|</span>
: null}

View File

@ -56,7 +56,6 @@ export default withTracker(() => {
},
});
const breakouts = Service.getBreakouts();
const currentUserId = Auth.userID;
const { connectRecordingObserver, processOutsideToggleRecording } = Service;
const currentUser = Users.findOne({ userId: Auth.userID });
@ -67,7 +66,6 @@ export default withTracker(() => {
return {
amIModerator,
isExpanded,
breakouts,
currentUserId,
processOutsideToggleRecording,
connectRecordingObserver,
@ -75,8 +73,6 @@ export default withTracker(() => {
presentationTitle: meetingTitle,
hasUnreadMessages: checkUnreadMessages(),
isBreakoutRoom: meetingIsBreakout(),
getBreakoutByUser: Service.getBreakoutByUser,
currentBreakoutUser: Service.getBreakoutUserByUserId(Auth.userID),
recordProps: meetingRecorded,
toggleUserList: () => {
Session.set('isUserListOpen', !isExpanded);

View File

@ -1,31 +1,6 @@
import Auth from '/imports/ui/services/auth';
import Breakouts from '/imports/api/breakouts';
import { makeCall } from '/imports/ui/services/api';
import Meetings from '/imports/api/meetings';
import fp from 'lodash/fp';
const getBreakoutByUserId = userId => Breakouts.find({ 'users.userId': userId }).fetch();
const getBreakoutByUser = user => Breakouts.findOne({ users: user });
const getUsersFromBreakouts = breakoutsArray => breakoutsArray
.map(breakout => breakout.users)
.reduce((acc, usersArray) => [...acc, usersArray], []);
const filterUserURLs = userId => breakoutUsersArray => breakoutUsersArray
.filter(user => user.userId === userId);
const getLastURLInserted = breakoutURLArray => breakoutURLArray
.sort((a, b) => a.insertedTime - b.insertedTime).pop();
const getBreakoutUserByUserId = userId => fp.pipe(
getBreakoutByUserId,
getUsersFromBreakouts,
filterUserURLs(userId),
getLastURLInserted,
)(userId);
const getBreakouts = () => Breakouts.find({}, { sort: { sequence: 1 } }).fetch();
const processOutsideToggleRecording = (e) => {
switch (e.data) {
@ -45,7 +20,6 @@ const processOutsideToggleRecording = (e) => {
}
};
const connectRecordingObserver = () => {
// notify on load complete
this.window.parent.postMessage({ response: 'readyToConnect' }, '*');
@ -54,7 +28,4 @@ const connectRecordingObserver = () => {
export default {
connectRecordingObserver: () => connectRecordingObserver(),
processOutsideToggleRecording: arg => processOutsideToggleRecording(arg),
getBreakoutUserByUserId,
getBreakoutByUser,
getBreakouts,
};

View File

@ -82,7 +82,3 @@
.btnSettings {
}
.dropdownBreakout {
cursor: pointer;
}

View File

@ -4,6 +4,7 @@ import Note from '/imports/api/note';
import Auth from '/imports/ui/services/auth';
import Settings from '/imports/ui/services/settings';
import mapUser from '/imports/ui/services/user/mapUser';
import { Session } from 'meteor/session';
const NOTE_CONFIG = Meteor.settings.public.note;
@ -66,14 +67,25 @@ const getNoteURL = () => {
return url;
};
const getRevs = () => {
const note = Note.findOne({ meetingId: Auth.meetingID });
return note ? note.revs : 0;
};
const isEnabled = () => {
const note = Note.findOne({ meetingId: Auth.meetingID });
return NOTE_CONFIG.enabled && note;
};
const isPanelOpened = () => {
return Session.get('openPanel') === 'note';
};
export default {
getNoteURL,
getReadOnlyURL,
isLocked,
isEnabled,
isPanelOpened,
getRevs,
};

View File

@ -8,8 +8,7 @@ import Meetings 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 NavBarService from '../nav-bar/service';
import breakoutService from '/imports/ui/components/breakout-room/service';
import NotificationsBar from './component';
// disconnected and trying to open a new connection
@ -167,7 +166,7 @@ export default injectIntl(withTracker(({ intl }) => {
}
const meetingId = Auth.meetingID;
const breakouts = NavBarService.getBreakouts();
const breakouts = breakoutService.getBreakouts();
if (breakouts.length > 0) {
const currentBreakout = breakouts.find(b => b.breakoutId === meetingId);
@ -186,7 +185,6 @@ export default injectIntl(withTracker(({ intl }) => {
}
}
const Meeting = Meetings.findOne({ meetingId: Auth.meetingID });
if (Meeting) {

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import browser from 'browser-detect';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import Button from '/imports/ui/components/button/component';
@ -17,10 +17,34 @@ const intlMessages = defineMessages({
id: 'app.presentation.presentationToolbar.prevSlideLabel',
description: 'Previous slide button label',
},
previousSlideDesc: {
id: 'app.presentation.presentationToolbar.prevSlideDesc',
description: 'Aria description for when switching to previous slide',
},
nextSlideLabel: {
id: 'app.presentation.presentationToolbar.nextSlideLabel',
description: 'Next slide button label',
},
nextSlideDesc: {
id: 'app.presentation.presentationToolbar.nextSlideDesc',
description: 'Aria description for when switching to next slide',
},
noNextSlideDesc: {
id: 'app.presentation.presentationToolbar.noNextSlideDesc',
description: '',
},
noPrevSlideDesc: {
id: 'app.presentation.presentationToolbar.noPrevSlideDesc',
description: '',
},
skipSlideLabel: {
id: 'app.presentation.presentationToolbar.skipSlideLabel',
description: 'Aria label for when switching to a specific slide',
},
skipSlideDesc: {
id: 'app.presentation.presentationToolbar.skipSlideDesc',
description: 'Aria description for when switching to a specific slide',
},
goToSlide: {
id: 'app.presentation.presentationToolbar.goToSlide',
description: 'button for slide select',
@ -33,10 +57,18 @@ const intlMessages = defineMessages({
id: 'app.presentation.presentationToolbar.fitToWidth',
description: 'button for fit to width',
},
fitToWidthDesc: {
id: 'app.presentation.presentationToolbar.fitWidthDesc',
description: 'Aria description to display the whole width of the slide',
},
fitToPage: {
id: 'app.presentation.presentationToolbar.fitToPage',
description: 'button label for fit to width',
},
fitToPageDesc: {
id: 'app.presentation.presentationToolbar.fitScreenDesc',
description: 'Aria description to display the whole slide',
},
presentationLabel: {
id: 'app.presentationUploder.title',
description: 'presentation area element label',
@ -44,104 +76,6 @@ const intlMessages = defineMessages({
});
class PresentationToolbar extends Component {
static renderAriaLabelsDescs() {
return (
<div hidden>
{/* Previous Slide button aria */}
<div id="prevSlideLabel">
<FormattedMessage
id="app.presentation.presentationToolbar.prevSlideLabel"
description="Aria label for when switching to previous slide"
defaultMessage="Previous slide"
/>
</div>
<div id="prevSlideDesc">
<FormattedMessage
id="app.presentation.presentationToolbar.prevSlideDesc"
description="Aria description for when switching to previous slide"
defaultMessage="Change the presentation to the previous slide"
/>
</div>
{/* Next Slide button aria */}
<div id="nextSlideLabel">
<FormattedMessage
id="app.presentation.presentationToolbar.nextSlideLabel"
description="Aria label for when switching to next slide"
defaultMessage="Next slide"
/>
</div>
<div id="nextSlideDesc">
<FormattedMessage
id="app.presentation.presentationToolbar.nextSlideDesc"
description="Aria description for when switching to next slide"
defaultMessage="Change the presentation to the next slide"
/>
</div>
{/* Skip Slide drop down aria */}
<div id="skipSlideLabel">
<FormattedMessage
id="app.presentation.presentationToolbar.skipSlideLabel"
description="Aria label for when switching to a specific slide"
defaultMessage="Skip slide"
/>
</div>
<div id="skipSlideDesc">
<FormattedMessage
id="app.presentation.presentationToolbar.skipSlideDesc"
description="Aria description for when switching to a specific slide"
defaultMessage="Change the presentation to a specific slide"
/>
</div>
{/* Fit to width button aria */}
<div id="fitWidthLabel">
<FormattedMessage
id="app.presentation.presentationToolbar.fitWidthLabel"
description="Aria description to display the whole width of the slide"
defaultMessage="Fit to width"
/>
</div>
<div id="fitWidthDesc">
<FormattedMessage
id="app.presentation.presentationToolbar.fitWidthDesc"
description="Aria description to display the whole width of the slide"
defaultMessage="Display the whole width of the slide"
/>
</div>
{/* Fit to screen button aria */}
<div id="fitScreenLabel">
<FormattedMessage
id="app.presentation.presentationToolbar.fitScreenLabel"
description="Aria label to display the whole slide"
defaultMessage="Fit to screen"
/>
</div>
<div id="fitScreenDesc">
<FormattedMessage
id="app.presentation.presentationToolbar.fitScreenDesc"
description="Aria label to display the whole slide"
defaultMessage="Display the whole slide"
/>
</div>
{/* Zoom slider aria */}
<div id="zoomLabel">
<FormattedMessage
id="app.presentation.presentationToolbar.zoomLabel"
description="Aria label to zoom presentation"
defaultMessage="Zoom"
/>
</div>
<div id="zoomDesc">
<FormattedMessage
id="app.presentation.presentationToolbar.zoomDesc"
description="Aria label to zoom presentation"
defaultMessage="Change the zoom level of the presentation"
/>
</div>
</div>
);
}
constructor(props) {
super(props);
@ -151,6 +85,7 @@ class PresentationToolbar extends Component {
this.handleValuesChange = this.handleValuesChange.bind(this);
this.handleSkipToSlideChange = this.handleSkipToSlideChange.bind(this);
this.change = this.change.bind(this);
this.renderAriaDescs = this.renderAriaDescs.bind(this);
this.switchSlide = this.switchSlide.bind(this);
this.setInt = 0;
}
@ -197,6 +132,36 @@ class PresentationToolbar extends Component {
zoomChanger(value);
}
renderAriaDescs() {
const { intl } = this.props;
return (
<div hidden>
{/* Aria description's for toolbar buttons */}
<div id="prevSlideDesc">
{intl.formatMessage(intlMessages.previousSlideDesc)}
</div>
<div id="noPrevSlideDesc">
{intl.formatMessage(intlMessages.noPrevSlideDesc)}
</div>
<div id="nextSlideDesc">
{intl.formatMessage(intlMessages.nextSlideDesc)}
</div>
<div id="noNextSlideDesc">
{intl.formatMessage(intlMessages.noNextSlideDesc)}
</div>
<div id="skipSlideDesc">
{intl.formatMessage(intlMessages.skipSlideDesc)}
</div>
<div id="fitWidthDesc">
{intl.formatMessage(intlMessages.fitToWidthDesc)}
</div>
<div id="fitPageDesc">
{intl.formatMessage(intlMessages.fitToPageDesc)}
</div>
</div>
);
}
renderSkipSlideOpts(numberOfSlides) {
// Fill drop down menu with all the slides in presentation
const { intl } = this.props;
@ -235,17 +200,28 @@ class PresentationToolbar extends Component {
const tooltipDistance = 35;
const startOfSlides = !(currentSlideNum > 1);
const endOfSlides = !(currentSlideNum < numberOfSlides);
const prevSlideAriaLabel = startOfSlides
? intl.formatMessage(intlMessages.previousSlideLabel)
: `${intl.formatMessage(intlMessages.previousSlideLabel)} (${currentSlideNum <= 1 ? '' : (currentSlideNum - 1)})`;
const nextSlideAriaLabel = endOfSlides
? intl.formatMessage(intlMessages.nextSlideLabel)
: `${intl.formatMessage(intlMessages.nextSlideLabel)} (${currentSlideNum >= 1 ? (currentSlideNum + 1) : ''})`;
return (
<div id="presentationToolbarWrapper" className={styles.presentationToolbarWrapper}>
{PresentationToolbar.renderAriaLabelsDescs()}
{this.renderAriaDescs()}
{<div />}
{
<div className={styles.presentationSlideControls}>
<Button
role="button"
aria-labelledby="prevSlideLabel"
aria-describedby="prevSlideDesc"
disabled={!(currentSlideNum > 1)}
aria-label={prevSlideAriaLabel}
aria-describedby={startOfSlides ? 'noPrevSlideDesc' : 'prevSlideDesc'}
disabled={startOfSlides}
color="default"
icon="left_arrow"
size="md"
@ -262,13 +238,8 @@ class PresentationToolbar extends Component {
className={styles.presentationBtn}
>
<select
role="button"
/*
<select> has an implicit role of listbox, no need to define
role="listbox" explicitly
*/
id="skipSlide"
aria-labelledby="skipSlideLabel"
aria-label={intl.formatMessage(intlMessages.skipSlideLabel)}
aria-describedby="skipSlideDesc"
aria-live="polite"
aria-relevant="all"
@ -281,9 +252,9 @@ class PresentationToolbar extends Component {
</Tooltip>
<Button
role="button"
aria-labelledby="nextSlideLabel"
aria-describedby="nextSlideDesc"
disabled={!(currentSlideNum < numberOfSlides)}
aria-label={nextSlideAriaLabel}
aria-describedby={endOfSlides ? 'noNextSlideDesc' : 'nextSlideDesc'}
disabled={endOfSlides}
color="default"
icon="right_arrow"
size="md"
@ -313,8 +284,11 @@ class PresentationToolbar extends Component {
}
<Button
role="button"
aria-labelledby="fitWidthLabel"
aria-describedby="fitWidthDesc"
aria-describedby={fitToWidth ? 'fitPageDesc' : 'fitWidthDesc'}
aria-label={fitToWidth
? `${intl.formatMessage(intlMessages.presentationLabel)} ${intl.formatMessage(intlMessages.fitToPage)}`
: `${intl.formatMessage(intlMessages.presentationLabel)} ${intl.formatMessage(intlMessages.fitToWidth)}`
}
color="default"
icon="fit_to_width"
size="md"

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import cx from 'classnames';
import { styles } from '../styles.scss';
@ -30,62 +30,13 @@ const intlMessages = defineMessages({
id: 'app.presentation.presentationToolbar.zoomOutDesc',
description: 'Aria description for decrement zoom level',
},
zoomIndicator: {
id: 'app.presentation.presentationToolbar.zoomIndicator',
description: 'Aria label for current zoom level',
currentValue: {
id: 'app.submenu.application.currentSize',
description: 'current presentation zoom percentage aria description',
},
});
class ZoomTool extends Component {
static renderAriaLabelsDescs() {
return (
<div hidden key="hidden-div">
<div id="zoomInLabel">
<FormattedMessage
id="app.presentation.presentationToolbar.zoomInLabel"
description="Aria label for increment zoom level"
defaultMessage="Increment zoom"
/>
</div>
<div id="zoomInDesc">
<FormattedMessage
id="app.presentation.presentationToolbar.zoomInDesc"
description="Aria description for increment zoom level"
defaultMessage="Increment zoom"
/>
</div>
<div id="zoomOutLabel">
<FormattedMessage
id="app.presentation.presentationToolbar.zoomOutLabel"
description="Aria label for decrement zoom level"
defaultMessage="Decrement zoom"
/>
</div>
<div id="zoomOutDesc">
<FormattedMessage
id="app.presentation.presentationToolbar.zoomOutDesc"
description="Aria description for decrement zoom level"
defaultMessage="Decrement zoom"
/>
</div>
<div id="zoomIndicator">
<FormattedMessage
id="app.presentation.presentationToolbar.zoomIndicator"
description="Aria label for current zoom level"
defaultMessage="Current zoom level"
/>
</div>
<div id="zoomReset">
<FormattedMessage
id="app.presentation.presentationToolbar.zoomReset"
description="Aria label for reset zoom level"
defaultMessage="Reset zoom level"
/>
</div>
</div>
);
}
constructor(props) {
super(props);
this.increment = this.increment.bind(this);
@ -195,8 +146,22 @@ class ZoomTool extends Component {
maxBound,
intl,
tooltipDistance,
step,
} = this.props;
const { stateZoomValue } = this.state;
let zoomOutAriaLabel = intl.formatMessage(intlMessages.zoomOutLabel);
if (zoomValue > minBound) {
zoomOutAriaLabel += ` ${intl.formatNumber(((zoomValue - step) / 100), { style: 'percent' })}`;
}
let zoomInAriaLabel = intl.formatMessage(intlMessages.zoomInLabel);
if (zoomValue < maxBound) {
zoomInAriaLabel += ` ${intl.formatNumber(((zoomValue + step) / 100), { style: 'percent' })}`;
}
const stateZoomPct = intl.formatNumber((stateZoomValue / 100), { style: 'percent' });
return (
[
(
@ -208,9 +173,8 @@ class ZoomTool extends Component {
>
<Button
key="zoom-tool-1"
aria-labelledby="zoomOutLabel"
aria-describedby="zoomOutDesc"
aria-label={intl.formatMessage(intlMessages.zoomOutLabel)}
aria-describedby="zoomOutDescription"
aria-label={zoomOutAriaLabel}
label={intl.formatMessage(intlMessages.zoomOutLabel)}
icon="substract"
onClick={() => { }}
@ -219,22 +183,28 @@ class ZoomTool extends Component {
tooltipDistance={tooltipDistance}
hideLabel
/>
<div id="zoomOutDescription" hidden>{intl.formatMessage(intlMessages.zoomOutDesc)}</div>
</HoldButton>
),
(
<Button
key="zoom-tool-2"
aria-labelledby="zoomReset"
aria-describedby={stateZoomValue}
color="default"
customIcon={`${stateZoomValue}%`}
size="md"
onClick={() => this.resetZoom()}
label={intl.formatMessage(intlMessages.resetZoomLabel)}
className={cx(styles.zoomPercentageDisplay, styles.presentationBtn)}
tooltipDistance={tooltipDistance}
hideLabel
/>
<span key="zoom-tool-2">
<Button
aria-label={intl.formatMessage(intlMessages.resetZoomLabel)}
aria-describedby="resetZoomDescription"
disabled={stateZoomValue === minBound}
color="default"
customIcon={stateZoomPct}
size="md"
onClick={() => this.resetZoom()}
label={intl.formatMessage(intlMessages.resetZoomLabel)}
className={cx(styles.zoomPercentageDisplay, styles.presentationBtn)}
tooltipDistance={tooltipDistance}
hideLabel
/>
<div id="resetZoomDescription" hidden>
{intl.formatMessage(intlMessages.currentValue, ({ 0: stateZoomPct }))}
</div>
</span>
),
(
<HoldButton
@ -245,9 +215,8 @@ class ZoomTool extends Component {
>
<Button
key="zoom-tool-3"
aria-labelledby="zoomInLabel"
aria-describedby="zoomInDesc"
aria-label={intl.formatMessage(intlMessages.zoomInLabel)}
aria-describedby="zoomInDescription"
aria-label={zoomInAriaLabel}
label={intl.formatMessage(intlMessages.zoomInLabel)}
icon="add"
onClick={() => { }}
@ -256,6 +225,7 @@ class ZoomTool extends Component {
tooltipDistance={tooltipDistance}
hideLabel
/>
<div id="zoomInDescription" hidden>{intl.formatMessage(intlMessages.zoomInDesc)}</div>
</HoldButton>
),
]
@ -264,11 +234,13 @@ class ZoomTool extends Component {
}
const propTypes = {
intl: intlShape.isRequired,
zoomValue: PropTypes.number.isRequired,
change: PropTypes.func.isRequired,
minBound: PropTypes.number.isRequired,
maxBound: PropTypes.number.isRequired,
step: PropTypes.number.isRequired,
tooltipDistance: PropTypes.number.isRequired,
};
ZoomTool.propTypes = propTypes;

View File

@ -18,6 +18,7 @@ import { styles } from './styles.scss';
const propTypes = {
intl: intlShape.isRequired,
mountModal: PropTypes.func.isRequired,
defaultFileName: PropTypes.string.isRequired,
fileSizeMin: PropTypes.number.isRequired,
fileSizeMax: PropTypes.number.isRequired,
@ -48,9 +49,13 @@ const intlMessages = defineMessages({
id: 'app.presentationUploder.message',
description: 'message warning the types of files accepted',
},
uploadLabel: {
id: 'app.presentationUploder.uploadLabel',
description: 'confirm label when presentations are to be uploaded',
},
confirmLabel: {
id: 'app.presentationUploder.confirmLabel',
description: 'used in the button that start the upload of the new presentation',
description: 'confirm label when no presentations are to be uploaded',
},
confirmDesc: {
id: 'app.presentationUploder.confirmDesc',
@ -174,6 +179,7 @@ class PresentationUploader extends Component {
oldCurrentId: currentPres ? currentPres.id : -1,
preventClosing: false,
disableActions: false,
disableConfirm: false,
};
this.handleConfirm = this.handleConfirm.bind(this);
@ -189,6 +195,15 @@ class PresentationUploader extends Component {
this.releaseActionsOnPresentationError = this.releaseActionsOnPresentationError.bind(this);
}
static getDerivedStateFromProps(props, state) {
if (props.presentations[0].isCurrent && state.disableConfirm) {
return {
disableConfirm: !state.disableConfirm,
};
}
return null;
}
componentDidUpdate() {
this.releaseActionsOnPresentationError();
}
@ -302,9 +317,12 @@ class PresentationUploader extends Component {
handleFiledrop(files, files2) {
const { fileValidMimeTypes, intl } = this.props;
const mimeTypes = fileValidMimeTypes.map(fileValid => fileValid.mime);
const validMimes = fileValidMimeTypes.map(fileValid => fileValid.mime);
const validExtentions = fileValidMimeTypes.map(fileValid => fileValid.extension);
const [accepted, rejected] = _.partition(files
.concat(files2), f => mimeTypes.includes(f.type));
.concat(files2), f => (
validMimes.includes(f.type) || validExtentions.includes(`.${f.name.split('.').pop()}`)
));
const presentationsToUpload = accepted.map((file) => {
const id = _.uniqueId(file.name);
@ -392,6 +410,7 @@ class PresentationUploader extends Component {
this.setState({
presentations: presentationsUpdated,
disableConfirm: false,
});
}
@ -405,6 +424,7 @@ class PresentationUploader extends Component {
presentations: update(presentations, {
$splice: [[toRemoveIndex, 1]],
}),
disableConfirm: true,
});
}
@ -448,7 +468,9 @@ class PresentationUploader extends Component {
<table className={styles.table}>
<thead>
<tr>
<th className={styles.visuallyHidden} colSpan={3}>{intl.formatMessage(intlMessages.filename)}</th>
<th className={styles.visuallyHidden} colSpan={3}>
{intl.formatMessage(intlMessages.filename)}
</th>
<th className={styles.visuallyHidden}>{intl.formatMessage(intlMessages.status)}</th>
<th className={styles.visuallyHidden}>{intl.formatMessage(intlMessages.options)}</th>
</tr>
@ -637,11 +659,13 @@ class PresentationUploader extends Component {
if (disableActions) return null;
return (
// Until the Dropzone package has fixed the mime type hover validation, the rejectClassName
// prop is being remove to prevent the error styles from being applied to valid file types.
// Error handling is being done in the onDrop prop.
<Dropzone
multiple
className={styles.dropzone}
activeClassName={styles.dropzoneActive}
rejectClassName={styles.dropzoneReject}
accept={isMobileBrowser ? '' : fileValidMimeTypes.map(fileValid => fileValid.extension)}
minSize={fileSizeMin}
maxSize={fileSizeMax}
@ -662,7 +686,19 @@ class PresentationUploader extends Component {
render() {
const { intl } = this.props;
const { preventClosing, disableActions } = this.state;
const {
preventClosing, disableActions, presentations, disableConfirm,
} = this.state;
let awaitingConversion = false;
presentations.map((presentation) => {
if (!presentation.conversion.done) awaitingConversion = true;
return null;
});
const confirmLabel = awaitingConversion
? intl.formatMessage(intlMessages.uploadLabel)
: intl.formatMessage(intlMessages.confirmLabel);
return (
<ModalFullscreen
@ -670,9 +706,9 @@ class PresentationUploader extends Component {
preventClosing={preventClosing}
confirm={{
callback: this.handleConfirm,
label: intl.formatMessage(intlMessages.confirmLabel),
label: confirmLabel,
description: intl.formatMessage(intlMessages.confirmDesc),
disabled: disableActions,
disabled: disableConfirm,
}}
dismiss={{
callback: this.handleDismiss,

View File

@ -216,13 +216,7 @@
}
.dropzoneActive {
background-color: var(--color-gray-light);
}
.dropzoneReject {
color: var(--color-danger);
background-color: var(--color-danger);
cursor: no-drop;
background-color: var(--color-gray-lighter);
}
.dropzoneIcon {

View File

@ -1,5 +1,6 @@
import Screenshare from '/imports/api/screenshare';
import KurentoBridge from '/imports/api/screenshare/client/bridge';
import Settings from '/imports/ui/services/settings';
// when the meeting information has been updated check to see if it was
// screensharing. If it has changed either trigger a call to receive video
@ -32,12 +33,14 @@ const shareScreen = (onFail) => {
KurentoBridge.kurentoShareScreen(onFail);
};
const screenShareEndAlert = () => new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename}/resources/sounds/ScreenshareOff.mp3`).play();
const unshareScreen = () => {
KurentoBridge.kurentoExitScreenShare();
screenShareEndAlert();
};
const screenShareEndAlert = () => new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename}/resources/sounds/ScreenshareOff.mp3`).play();
const dataSavingSetting = () => Settings.dataSaving.viewScreenshare;
export {
isVideoBroadcasting,
@ -46,4 +49,5 @@ export {
shareScreen,
unshareScreen,
screenShareEndAlert,
dataSavingSetting,
};

View File

@ -242,22 +242,20 @@ class ApplicationMenu extends BaseMenu {
<div className={styles.row}>
<div className={styles.col} aria-hidden="true">
<div className={styles.formElement}>
<label className={styles.label}>
<label
className={styles.label}
htmlFor="langSelector"
aria-label={intl.formatMessage(intlMessages.ariaLanguageLabel)}
>
{intl.formatMessage(intlMessages.languageLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div
id="changeLangLabel"
aria-label={intl.formatMessage(intlMessages.ariaLanguageLabel)}
/>
<label
aria-labelledby="changeLangLabel"
className={cx(styles.formElement, styles.pullContentRight)}
>
<span className={cx(styles.formElement, styles.pullContentRight)}>
{availableLocales && availableLocales.length > 0 ? (
<select
id="langSelector"
defaultValue={this.state.settings.locale}
lang={this.state.settings.locale}
className={styles.select}
@ -271,7 +269,7 @@ class ApplicationMenu extends BaseMenu {
))}
</select>
) : null}
</label>
</span>
</div>
</div>
<hr className={styles.separator} />

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Auth from '/imports/ui/services/auth';
import logger from '/imports/startup/client/logger';
@ -19,7 +19,7 @@ const SUBSCRIPTIONS = [
'network-information',
];
class Subscriptions extends Component {
class Subscriptions extends React.Component {
componentDidUpdate() {
const { subscriptionsReady } = this.props;
if (subscriptionsReady) {
@ -36,7 +36,11 @@ class Subscriptions extends Component {
export default withTracker(() => {
const { credentials } = Auth;
const { meetingId, requesterUserId } = credentials;
if (Session.get('codeError')) {
return {
subscriptionsReady: true,
};
}
const subscriptionErrorHandler = {
onError: (error) => {
logger.error({ logCode: 'startup_client_subscription_error' }, error);
@ -44,7 +48,11 @@ export default withTracker(() => {
},
};
const subscriptionsHandlers = SUBSCRIPTIONS.map(name => Meteor.subscribe(name, credentials, subscriptionErrorHandler));
const subscriptionsHandlers = SUBSCRIPTIONS.map(name => Meteor.subscribe(
name,
credentials,
subscriptionErrorHandler,
));
let groupChatMessageHandler = {};
// let annotationsHandler = {};

View File

@ -2,9 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import Icon from '/imports/ui/components/icon/component';
import { Session } from 'meteor/session';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import { styles } from './styles';
const propTypes = {
intl: intlShape.isRequired,
locale: PropTypes.shape({
locale: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
@ -12,6 +14,13 @@ const propTypes = {
tabIndex: PropTypes.number.isRequired,
};
const intlMessages = defineMessages({
captionLabel: {
id: 'app.captions.label',
description: 'used for captions button aria label',
},
});
const handleClickToggleCaptions = (locale) => {
const panel = Session.get('openPanel');
@ -30,6 +39,7 @@ const handleClickToggleCaptions = (locale) => {
const CaptionsListItem = (props) => {
const {
intl,
locale,
tabIndex,
} = props;
@ -41,13 +51,14 @@ const CaptionsListItem = (props) => {
id={locale.locale}
className={styles.captionsListItem}
onClick={() => handleClickToggleCaptions(locale.locale)}
aria-label={`${locale.name} ${intl.formatMessage(intlMessages.captionLabel)}`}
>
<Icon iconName="polling" />
<span>{locale.name}</span>
<span aria-hidden>{locale.name}</span>
</div>
);
};
CaptionsListItem.propTypes = propTypes;
export default CaptionsListItem;
export default injectIntl(CaptionsListItem);

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