Merge branch 'master' of https://github.com/bigbluebutton/bigbluebutton into _summit-adjust-screen-reader-trap
This commit is contained in:
commit
ffecbfa6ac
@ -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");
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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")) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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/";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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>
|
||||
|
10
bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/svc2/resign-ffmpeg-jar.sh
Executable file
10
bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/svc2/resign-ffmpeg-jar.sh
Executable 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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
10
bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/svc2/resign-ffmpeg-jar.sh
Executable file
10
bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/svc2/resign-ffmpeg-jar.sh
Executable 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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
BIN
bbb-screenshare/jws/native-libs/unsigned-jars/javacv-screenshare-0.0.1.jar
Executable file
BIN
bbb-screenshare/jws/native-libs/unsigned-jars/javacv-screenshare-0.0.1.jar
Executable file
Binary file not shown.
12
bbb-screenshare/jws/native-libs/unsigned-jars/resign-ffmpeg-jar.sh
Executable file
12
bbb-screenshare/jws/native-libs/unsigned-jars/resign-ffmpeg-jar.sh
Executable 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
|
||||
|
12
bbb-screenshare/jws/native-libs/unsigned-jars/resign-screenshare.sh
Executable file
12
bbb-screenshare/jws/native-libs/unsigned-jars/resign-screenshare.sh
Executable 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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
5
bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js
Normal file → Executable file
5
bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js
Normal file → Executable 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,
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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));
|
@ -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);
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
@ -1 +1,2 @@
|
||||
import './publishers';
|
||||
import './eventHandlers';
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
}
|
@ -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`);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
|
@ -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 />
|
||||
|
@ -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)));
|
||||
|
||||
|
@ -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)));
|
||||
|
@ -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}
|
||||
|
@ -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)));
|
||||
|
@ -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))));
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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);
|
@ -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);
|
@ -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,
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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)}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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,
|
||||
};
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
@ -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])
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -82,7 +82,3 @@
|
||||
|
||||
.btnSettings {
|
||||
}
|
||||
|
||||
.dropdownBreakout {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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} />
|
||||
|
@ -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 = {};
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user