Merge branch 'master' of https://github.com/bigbluebutton/bigbluebutton into fixdisplay
This commit is contained in:
commit
106578f9dd
@ -44,7 +44,8 @@ class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
|
||||
liveMeeting.guestsWaiting,
|
||||
GuestPolicy(props.usersProp.guestPolicy, SystemUser.ID))
|
||||
|
||||
val outMsgRouter = new OutMsgRouter(props.recordProp.record, outGW)
|
||||
private val recordEvents = props.recordProp.record || props.recordProp.keepEvents
|
||||
val outMsgRouter = new OutMsgRouter(recordEvents, outGW)
|
||||
|
||||
val actorRef = context.actorOf(MeetingActor.props(props, eventBus, outMsgRouter, liveMeeting), props.meetingProp.intId)
|
||||
|
||||
|
@ -13,7 +13,7 @@ case class BreakoutProps(parentId: String, sequence: Int, freeJoin: Boolean, bre
|
||||
|
||||
case class PasswordProp(moderatorPass: String, viewerPass: String)
|
||||
|
||||
case class RecordProp(record: Boolean, autoStartRecording: Boolean, allowStartStopRecording: Boolean)
|
||||
case class RecordProp(record: Boolean, autoStartRecording: Boolean, allowStartStopRecording: Boolean, keepEvents: Boolean)
|
||||
|
||||
case class WelcomeProp(welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMessage: String)
|
||||
|
||||
|
@ -34,6 +34,7 @@ trait TestFixtures {
|
||||
val dialNumber = "613-555-1234"
|
||||
val maxUsers = 25
|
||||
val muteOnStart = false
|
||||
val keepEvents = false
|
||||
val guestPolicy = "ALWAYS_ASK"
|
||||
val metadata: collection.immutable.Map[String, String] = Map("foo" -> "bar", "bar" -> "baz", "baz" -> "foo")
|
||||
|
||||
@ -46,7 +47,7 @@ trait TestFixtures {
|
||||
userInactivityInspectTimerInMinutes = userInactivityInspectTimerInMinutes, userInactivityThresholdInMinutes = userInactivityInspectTimerInMinutes, userActivitySignResponseDelayInMinutes = userActivitySignResponseDelayInMinutes)
|
||||
val password = PasswordProp(moderatorPass = moderatorPassword, viewerPass = viewerPassword)
|
||||
val recordProp = RecordProp(record = record, autoStartRecording = autoStartRecording,
|
||||
allowStartStopRecording = allowStartStopRecording)
|
||||
allowStartStopRecording = allowStartStopRecording, keepEvents = keepEvents)
|
||||
val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg,
|
||||
modOnlyMessage = modOnlyMessage)
|
||||
val voiceProp = VoiceProp(telVoice = voiceConfId, voiceConf = voiceConfId, dialNumber = dialNumber, muteOnStart = muteOnStart)
|
||||
|
@ -110,6 +110,7 @@ public class MeetingService implements MessageListener {
|
||||
private StunTurnService stunTurnService;
|
||||
private RedisStorageService storeService;
|
||||
private CallbackUrlService callbackUrlService;
|
||||
private boolean keepEvents;
|
||||
|
||||
private ParamsProcessorUtil paramsProcessorUtil;
|
||||
private PresentationUrlDownloadService presDownloadService;
|
||||
@ -252,16 +253,20 @@ public class MeetingService implements MessageListener {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean storeEvents(Meeting m) {
|
||||
return m.isRecord() || keepEvents;
|
||||
}
|
||||
|
||||
private void handleCreateMeeting(Meeting m) {
|
||||
if (m.isBreakout()) {
|
||||
Meeting parent = meetings.get(m.getParentMeetingId());
|
||||
parent.addBreakoutRoom(m.getExternalId());
|
||||
if (parent.isRecord()) {
|
||||
if (storeEvents(parent)) {
|
||||
storeService.addBreakoutRoom(parent.getInternalId(), m.getInternalId());
|
||||
}
|
||||
}
|
||||
|
||||
if (m.isRecord()) {
|
||||
if (storeEvents(m)) {
|
||||
Map<String, String> metadata = new TreeMap<>();
|
||||
metadata.putAll(m.getMetadata());
|
||||
// TODO: Need a better way to store these values for recordings
|
||||
@ -310,7 +315,7 @@ public class MeetingService implements MessageListener {
|
||||
m.getDialNumber(), m.getMaxUsers(), m.getMaxInactivityTimeoutMinutes(), m.getWarnMinutesBeforeMax(),
|
||||
m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getmeetingExpireWhenLastUserLeftInMinutes(),
|
||||
m.getUserInactivityInspectTimerInMinutes(), m.getUserActivitySignResponseDelayInMinutes(),
|
||||
m.getUserInactivityThresholdInMinutes(), m.getMuteOnStart());
|
||||
m.getUserInactivityThresholdInMinutes(), m.getMuteOnStart(), keepEvents);
|
||||
}
|
||||
|
||||
private String formatPrettyDate(Long timestamp) {
|
||||
@ -522,6 +527,11 @@ public class MeetingService implements MessageListener {
|
||||
if (m != null) {
|
||||
m.setForciblyEnded(true);
|
||||
processRecording(m);
|
||||
if (keepEvents) {
|
||||
// The creation of the ended tag must occur after the creation of the
|
||||
// recorded tag to avoid concurrency issues at the recording scripts
|
||||
recordingService.markAsEnded(m.getInternalId());
|
||||
}
|
||||
destroyMeeting(m.getInternalId());
|
||||
meetings.remove(m.getInternalId());
|
||||
removeUserSessions(m.getInternalId());
|
||||
@ -985,4 +995,8 @@ public class MeetingService implements MessageListener {
|
||||
public void setStunTurnService(StunTurnService s) {
|
||||
stunTurnService = s;
|
||||
}
|
||||
|
||||
public void setKeepEvents(boolean value) {
|
||||
keepEvents = value;
|
||||
}
|
||||
}
|
||||
|
@ -126,6 +126,23 @@ public class RecordingService {
|
||||
}
|
||||
}
|
||||
|
||||
public void markAsEnded(String meetingId) {
|
||||
String done = recordStatusDir + "/../ended/" + meetingId + ".done";
|
||||
|
||||
File doneFile = new File(done);
|
||||
if (!doneFile.exists()) {
|
||||
try {
|
||||
doneFile.createNewFile();
|
||||
if (!doneFile.exists())
|
||||
log.error("Failed to create " + done + " file.");
|
||||
} catch (IOException e) {
|
||||
log.error("Exception occured when trying to create {} file.", done);
|
||||
}
|
||||
} else {
|
||||
log.error(done + " file already exists.");
|
||||
}
|
||||
}
|
||||
|
||||
public List<RecordingMetadata> getRecordingsMetadata(List<String> recordIDs, List<String> states) {
|
||||
List<RecordingMetadata> recs = new ArrayList<>();
|
||||
|
||||
|
@ -25,7 +25,8 @@ public interface IBbbWebApiGWApp {
|
||||
Integer userInactivityInspectTimerInMinutes,
|
||||
Integer userInactivityThresholdInMinutes,
|
||||
Integer userActivitySignResponseDelayInMinutes,
|
||||
Boolean muteOnStart);
|
||||
Boolean muteOnStart,
|
||||
Boolean keepEvents);
|
||||
|
||||
void registerUser(String meetingID, String internalUserId, String fullname, String role,
|
||||
String externUserID, String authToken, String avatarURL,
|
||||
|
@ -91,7 +91,8 @@ class BbbWebApiGWApp(
|
||||
userInactivityInspectTimerInMinutes: java.lang.Integer,
|
||||
userInactivityThresholdInMinutes: java.lang.Integer,
|
||||
userActivitySignResponseDelayInMinutes: java.lang.Integer,
|
||||
muteOnStart: java.lang.Boolean): Unit = {
|
||||
muteOnStart: java.lang.Boolean,
|
||||
keepEvents: java.lang.Boolean): Unit = {
|
||||
|
||||
val meetingProp = MeetingProp(name = meetingName, extId = extMeetingId, intId = meetingId,
|
||||
isBreakout = isBreakout.booleanValue())
|
||||
@ -108,7 +109,7 @@ class BbbWebApiGWApp(
|
||||
|
||||
val password = PasswordProp(moderatorPass = moderatorPass, viewerPass = viewerPass)
|
||||
val recordProp = RecordProp(record = recorded.booleanValue(), autoStartRecording = autoStartRecording.booleanValue(),
|
||||
allowStartStopRecording = allowStartStopRecording.booleanValue())
|
||||
allowStartStopRecording = allowStartStopRecording.booleanValue(), keepEvents = keepEvents.booleanValue())
|
||||
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence.intValue(), freeJoin = freeJoin.booleanValue(), breakoutRooms = Vector())
|
||||
val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg,
|
||||
modOnlyMessage = modOnlyMessage)
|
||||
|
0
bbb-fsesl-client/deploy.sh
Normal file → Executable file
0
bbb-fsesl-client/deploy.sh
Normal file → Executable file
@ -63,7 +63,8 @@ public class CallAgent extends CallListenerAdapter implements CallStreamObserver
|
||||
private String _destination;
|
||||
private Boolean listeningToGlobal = false;
|
||||
private IMessagingService messagingService;
|
||||
|
||||
private ForceHangupGlobalAudioUsersListener forceHangupGlobalAudioUsersListener;
|
||||
|
||||
private enum CallState {
|
||||
UA_IDLE(0), UA_INCOMING_CALL(1), UA_OUTGOING_CALL(2), UA_ONCALL(3);
|
||||
private final int state;
|
||||
@ -404,15 +405,23 @@ public class CallAgent extends CallListenerAdapter implements CallStreamObserver
|
||||
|
||||
/** Callback function called when arriving a BYE request */
|
||||
public void onCallClosing(Call call, Message bye) {
|
||||
log.info("Received a BYE from the other end telling us to hangup.");
|
||||
|
||||
if (!isCurrentCall(call)) return;
|
||||
closeVoiceStreams();
|
||||
notifyListenersOfOnCallClosed();
|
||||
callState = CallState.UA_IDLE;
|
||||
log.info("Received a BYE from the other end telling us to hangup.");
|
||||
|
||||
// Reset local sdp for next call.
|
||||
initSessionDescriptor();
|
||||
if (!isCurrentCall(call)) return;
|
||||
closeVoiceStreams();
|
||||
notifyListenersOfOnCallClosed();
|
||||
|
||||
// FreeSWITCH initiated hangup of call. Hangup all listen only users.
|
||||
// ralam jan 24, 2019
|
||||
if (forceHangupGlobalAudioUsersListener != null) {
|
||||
log.info("Forcing hangup for listen only users of of voice conf {}.", getDestination());
|
||||
forceHangupGlobalAudioUsersListener.forceHangupGlobalAudioUsers(getDestination());
|
||||
}
|
||||
|
||||
callState = CallState.UA_IDLE;
|
||||
|
||||
// Reset local sdp for next call.
|
||||
initSessionDescriptor();
|
||||
}
|
||||
|
||||
|
||||
@ -451,7 +460,11 @@ public class CallAgent extends CallListenerAdapter implements CallStreamObserver
|
||||
private boolean isCurrentCall(Call call) {
|
||||
return this.call == call;
|
||||
}
|
||||
|
||||
|
||||
public void setForceHangupGlobalAudioUsersListener(ForceHangupGlobalAudioUsersListener listener) {
|
||||
forceHangupGlobalAudioUsersListener = listener;
|
||||
}
|
||||
|
||||
public void setCallStreamFactory(CallStreamFactory csf) {
|
||||
this.callStreamFactory = csf;
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
package org.bigbluebutton.voiceconf.sip;
|
||||
|
||||
public interface ForceHangupGlobalAudioUsersListener {
|
||||
void forceHangupGlobalAudioUsers(String voiceConf);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package org.bigbluebutton.voiceconf.sip;
|
||||
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.red5.app.sip.codecs.Codec;
|
||||
@ -80,6 +81,13 @@ public class GlobalCall {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized Collection<ListenOnlyUser> getAllListenOnlyUsers(String voiceConf) {
|
||||
if (voiceConfToListenOnlyUsersMap.containsKey(voiceConf)) {
|
||||
return voiceConfToListenOnlyUsersMap.get(voiceConf).getAllListenOnlyUsers();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Codec getRoomCodec(String roomName) {
|
||||
return roomToCodecMap.get(roomName);
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ import org.red5.server.api.stream.IBroadcastStream;
|
||||
* @author Richard Alam
|
||||
*
|
||||
*/
|
||||
public class SipPeer implements SipRegisterAgentListener {
|
||||
public class SipPeer implements SipRegisterAgentListener, ForceHangupGlobalAudioUsersListener {
|
||||
private static Logger log = Red5LoggerFactory.getLogger(SipPeer.class, "sip");
|
||||
|
||||
private ClientConnectionManager clientConnManager;
|
||||
@ -130,6 +130,7 @@ public class SipPeer implements SipRegisterAgentListener {
|
||||
SipPeerProfile callerProfile = SipPeerProfile.copy(registeredProfile);
|
||||
CallAgent ca = new CallAgent(this.clientRtpIp, sipProvider, callerProfile, audioconfProvider, clientId, messagingService);
|
||||
ca.setClientConnectionManager(clientConnManager);
|
||||
ca.setForceHangupGlobalAudioUsersListener(this);
|
||||
ca.setCallStreamFactory(callStreamFactory);
|
||||
callManager.add(ca);
|
||||
|
||||
@ -229,6 +230,15 @@ public class SipPeer implements SipRegisterAgentListener {
|
||||
log.info("Successfully unregistered with Sip Server");
|
||||
registered = false;
|
||||
}
|
||||
|
||||
public void forceHangupGlobalAudioUsers(String voiceConf) {
|
||||
Collection<ListenOnlyUser> listenOnlyUsers = GlobalCall.getAllListenOnlyUsers(voiceConf);
|
||||
Iterator iter = listenOnlyUsers.iterator();
|
||||
while (iter.hasNext()) {
|
||||
ListenOnlyUser listenOnlyUser = (ListenOnlyUser) iter.next();
|
||||
hangup(listenOnlyUser.clientId, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void setCallStreamFactory(CallStreamFactory csf) {
|
||||
callStreamFactory = csf;
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.bigbluebutton.voiceconf.sip;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@ -23,4 +25,8 @@ public class VoiceConfToListenOnlyUsersMap {
|
||||
public int numUsers() {
|
||||
return listenOnlyUsers.size();
|
||||
}
|
||||
|
||||
public Collection<ListenOnlyUser> getAllListenOnlyUsers() {
|
||||
return listenOnlyUsers.values();
|
||||
}
|
||||
}
|
||||
|
2
bigbluebutton-client/resources/config.xml.template
Normal file → Executable file
2
bigbluebutton-client/resources/config.xml.template
Normal file → Executable file
@ -11,7 +11,7 @@
|
||||
localesConfig="http://HOST/client/conf/locales.xml"
|
||||
localesDirectory="http://HOST/client/locale/"/>
|
||||
<skinning url="http://HOST/client/branding/css/V2Theme.css.swf?v=VERSION" />
|
||||
<branding logo="logos/logo.swf" copyright="© 2018 <u><a href="http://HOST/home.html" target="_blank">BigBlueButton Inc.</a></u> (build {0})" background="" toolbarColor="" showQuote="true"/>
|
||||
<branding logo="logos/logo.swf" copyright="© 2019 <u><a href="http://HOST/home.html" target="_blank">BigBlueButton Inc.</a></u> (build {0})" background="" toolbarColor="" showQuote="true"/>
|
||||
<shortcutKeys showButton="true" />
|
||||
<browserVersions chrome="CHROME_VERSION" firefox="FIREFOX_VERSION" flash="FLASH_VERSION"/>
|
||||
<layout showLogButton="false" defaultLayout="bbb.layout.name.defaultlayout"
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -534,12 +534,15 @@ function make_call(username, voiceBridge, server, callback, recall, isListenOnly
|
||||
currentSession.mediaHandler.on('iceConnectionConnected', function() {
|
||||
console.log('Received ICE status changed to connected');
|
||||
if (callICEConnected === false) {
|
||||
callICEConnected = true;
|
||||
clearTimeout(iceConnectedTimeout);
|
||||
if (callActive === true) {
|
||||
callback({'status':'started'});
|
||||
// Edge is only ready once the status is 'completed' so we need to skip this step
|
||||
if (!bowser.msedge) {
|
||||
callICEConnected = true;
|
||||
clearTimeout(iceConnectedTimeout);
|
||||
if (callActive === true) {
|
||||
callback({'status':'started'});
|
||||
}
|
||||
clearTimeout(callTimeout);
|
||||
}
|
||||
clearTimeout(callTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
@ -588,11 +591,7 @@ function releaseUserMedia() {
|
||||
}
|
||||
|
||||
function isWebRTCAvailable() {
|
||||
if (bowser.msedge) {
|
||||
return false;
|
||||
} else {
|
||||
return SIP.WebRTC.isSupported();
|
||||
}
|
||||
return SIP.WebRTC.isSupported();
|
||||
}
|
||||
|
||||
function getCallStatus() {
|
||||
|
@ -2,6 +2,8 @@ const isFirefox = typeof window.InstallTrigger !== 'undefined';
|
||||
const isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
|
||||
const isChrome = !!window.chrome && !isOpera;
|
||||
const isSafari = navigator.userAgent.indexOf('Safari') >= 0 && !isChrome;
|
||||
const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function'
|
||||
|| (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function'));
|
||||
const kurentoHandler = null;
|
||||
const SEND_ROLE = "send";
|
||||
const RECV_ROLE = "recv";
|
||||
@ -610,56 +612,86 @@ Kurento.normalizeCallback = function (callback) {
|
||||
|
||||
// this function explains how to use above methods/objects
|
||||
window.getScreenConstraints = function (sendSource, callback) {
|
||||
const screenConstraints = { video: {}, audio: false };
|
||||
let screenConstraints = { video: {}, audio: false };
|
||||
|
||||
// Limiting FPS to a range of 5-10 (5 ideal)
|
||||
screenConstraints.video.frameRate = { ideal: 5, max: 10 };
|
||||
|
||||
screenConstraints.video.height = { max: kurentoManager.kurentoScreenshare.vid_max_height };
|
||||
screenConstraints.video.width = { max: kurentoManager.kurentoScreenshare.vid_max_width };
|
||||
|
||||
const getDisplayMediaConstraints = function () {
|
||||
// The fine-grained constraints (e.g.: frameRate) are supposed to go into
|
||||
// the MediaStream because getDisplayMedia does not support them,
|
||||
// so they're passed differently
|
||||
kurentoManager.kurentoScreenshare.extensionInstalled = true;
|
||||
optionalConstraints.width = { max: kurentoManager.kurentoScreenshare.vid_max_width };
|
||||
optionalConstraints.height = { max: kurentoManager.kurentoScreenshare.vid_max_height };
|
||||
optionalConstraints.frameRate = { ideal: 5, max: 10 };
|
||||
|
||||
let gDPConstraints = {
|
||||
video: true,
|
||||
optional: optionalConstraints
|
||||
}
|
||||
|
||||
return gDPConstraints;
|
||||
};
|
||||
|
||||
const optionalConstraints = [
|
||||
{ googCpuOveruseDetection: true },
|
||||
{ googCpuOveruseEncodeUsage: true },
|
||||
{ googCpuUnderuseThreshold: 55 },
|
||||
{ googCpuOveruseThreshold: 100 },
|
||||
{ googPayloadPadding: true },
|
||||
{ googScreencastMinBitrate: 600 },
|
||||
{ googHighStartBitrate: true },
|
||||
{ googHighBitrate: true },
|
||||
{ googVeryHighBitrate: true },
|
||||
];
|
||||
|
||||
if (isChrome) {
|
||||
getChromeScreenConstraints((constraints) => {
|
||||
if (!constraints) {
|
||||
document.dispatchEvent(new Event('installChromeExtension'));
|
||||
return;
|
||||
}
|
||||
if (!hasDisplayMedia) {
|
||||
getChromeScreenConstraints((constraints) => {
|
||||
if (!constraints) {
|
||||
document.dispatchEvent(new Event('installChromeExtension'));
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceId = constraints.streamId;
|
||||
const sourceId = constraints.streamId;
|
||||
|
||||
kurentoManager.kurentoScreenshare.extensionInstalled = true;
|
||||
kurentoManager.kurentoScreenshare.extensionInstalled = true;
|
||||
|
||||
// this statement sets gets 'sourceId" and sets "chromeMediaSourceId"
|
||||
screenConstraints.video.chromeMediaSource = { exact: [sendSource] };
|
||||
screenConstraints.video.chromeMediaSourceId = sourceId;
|
||||
screenConstraints.optional = [
|
||||
{ googCpuOveruseDetection: true },
|
||||
{ googCpuOveruseEncodeUsage: true },
|
||||
{ googCpuUnderuseThreshold: 55 },
|
||||
{ googCpuOveruseThreshold: 100},
|
||||
{ googPayloadPadding: true },
|
||||
{ googScreencastMinBitrate: 600 },
|
||||
{ googHighStartBitrate: true },
|
||||
{ googHighBitrate: true },
|
||||
{ googVeryHighBitrate: true }
|
||||
];
|
||||
// Re-wrap the video constraints into the mandatory object (latest adapter)
|
||||
screenConstraints.video = {}
|
||||
screenConstraints.video.mandatory = {};
|
||||
screenConstraints.video.mandatory.maxFrameRate = 10;
|
||||
screenConstraints.video.mandatory.maxHeight = kurentoManager.kurentoScreenshare.vid_max_height;
|
||||
screenConstraints.video.mandatory.maxWidth = kurentoManager.kurentoScreenshare.vid_max_width;
|
||||
screenConstraints.video.mandatory.chromeMediaSource = sendSource;
|
||||
screenConstraints.video.mandatory.chromeMediaSourceId = sourceId;
|
||||
screenConstraints.optional = optionalConstraints;
|
||||
|
||||
console.log('getScreenConstraints for Chrome returns => ', screenConstraints);
|
||||
// now invoking native getUserMedia API
|
||||
callback(null, screenConstraints);
|
||||
}, chromeExtension);
|
||||
console.log('getScreenConstraints for Chrome returns => ', screenConstraints);
|
||||
// now invoking native getUserMedia API
|
||||
callback(null, screenConstraints);
|
||||
}, chromeExtension);
|
||||
} else {
|
||||
return callback(null, getDisplayMediaConstraints());
|
||||
}
|
||||
} else if (isFirefox) {
|
||||
screenConstraints.video.mediaSource = 'window';
|
||||
|
||||
console.log('getScreenConstraints for Firefox returns => ', screenConstraints);
|
||||
// now invoking native getUserMedia API
|
||||
callback(null, screenConstraints);
|
||||
} else if (isSafari) {
|
||||
} else if (isSafari && !hasDisplayMedia) {
|
||||
screenConstraints.video.mediaSource = 'screen';
|
||||
|
||||
console.log('getScreenConstraints for Safari returns => ', screenConstraints);
|
||||
// now invoking native getUserMedia API
|
||||
callback(null, screenConstraints);
|
||||
} else if (hasDisplayMedia) {
|
||||
// Falls back to getDisplayMedia if the browser supports it
|
||||
return callback(null, getDisplayMediaConstraints());
|
||||
}
|
||||
};
|
||||
|
||||
@ -735,6 +767,10 @@ window.checkIfIncognito = function(isIncognito, isNotIncognito = function () {})
|
||||
window.checkChromeExtInstalled = function (callback, chromeExtensionId) {
|
||||
callback = Kurento.normalizeCallback(callback);
|
||||
|
||||
if (hasDisplayMedia) {
|
||||
return callback(true);
|
||||
}
|
||||
|
||||
if (typeof chrome === "undefined" || !chrome || !chrome.runtime) {
|
||||
// No API, so no extension for sure
|
||||
callback(false);
|
||||
|
@ -410,7 +410,27 @@ function WebRtcPeer(mode, options, callback) {
|
||||
return callback(error);
|
||||
constraints = [mediaConstraints];
|
||||
constraints.unshift(constraints_);
|
||||
getMedia(recursive.apply(undefined, constraints));
|
||||
let gDMCallback = function(stream) {
|
||||
stream.getTracks()[0].applyConstraints(constraints[0].optional)
|
||||
.then(() => {
|
||||
videoStream = stream;
|
||||
start();
|
||||
}).catch(() => {
|
||||
videoStream = stream;
|
||||
start();
|
||||
});
|
||||
}
|
||||
if (typeof navigator.getDisplayMedia === 'function') {
|
||||
navigator.getDisplayMedia(recursive.apply(undefined, constraints))
|
||||
.then(gDMCallback)
|
||||
.catch(callback);
|
||||
} else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
|
||||
navigator.mediaDevices.getDisplayMedia(recursive.apply(undefined, constraints))
|
||||
.then(gDMCallback)
|
||||
.catch(callback);
|
||||
} else {
|
||||
getMedia(recursive.apply(undefined, constraints));
|
||||
}
|
||||
}, guid);
|
||||
}
|
||||
} else {
|
||||
@ -1045,7 +1065,7 @@ module.exports = function(stream, options) {
|
||||
harker.setInterval = function(i) {
|
||||
interval = i;
|
||||
};
|
||||
|
||||
|
||||
harker.stop = function() {
|
||||
running = false;
|
||||
harker.emit('volume_change', -100, threshold);
|
||||
@ -1063,12 +1083,12 @@ module.exports = function(stream, options) {
|
||||
// and emit events if changed
|
||||
var looper = function() {
|
||||
setTimeout(function() {
|
||||
|
||||
|
||||
//check if stop has been called
|
||||
if(!running) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var currentVolume = getMaxVolume(analyser, fftBins);
|
||||
|
||||
harker.emit('volume_change', currentVolume, threshold);
|
||||
@ -4359,4 +4379,4 @@ WildEmitter.mixin = function (constructor) {
|
||||
WildEmitter.mixin(WildEmitter);
|
||||
|
||||
},{}]},{},[2])(2)
|
||||
});
|
||||
});
|
||||
|
@ -110,6 +110,8 @@ package org.bigbluebutton.modules.screenshare.utils
|
||||
ExternalInterface.addCallback("onSuccess", onSuccess);
|
||||
// check if the extension exists
|
||||
ExternalInterface.call("checkChromeExtInstalled", "onSuccess", WebRTCScreenshareUtility.chromeExtensionKey);
|
||||
} else if (BrowserCheck.isEdge()) {
|
||||
webRTCWorksAndConfigured("Edge, lets try");
|
||||
} else {
|
||||
cannotUseWebRTC("Web browser doesn't support WebRTC");
|
||||
return;
|
||||
|
@ -65,6 +65,10 @@ package org.bigbluebutton.util.browser {
|
||||
public static function isFirefox():Boolean {
|
||||
return _browserName.toLowerCase() == "firefox";
|
||||
}
|
||||
|
||||
public static function isEdge():Boolean {
|
||||
return _browserName.toLowerCase() == "edge";
|
||||
}
|
||||
|
||||
public static function isPuffinBelow46():Boolean {
|
||||
return _browserName.toLowerCase() == "puffin" && String(_fullVersion).substr(0, 3) < "4.6";
|
||||
|
@ -72,6 +72,15 @@ if [ "$(id -u)" != "0" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f /etc/bigbluebutton/bigbluebutton-release ]; then
|
||||
echo "#"
|
||||
echo "# BigBlueButton does not appear to be installed. Could not"
|
||||
echo "# locate:"
|
||||
echo "#"
|
||||
echo "# /usr/share/red5/webapps/bigbluebutton/WEB-INF/red5-web.xml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source /etc/bigbluebutton/bigbluebutton-release
|
||||
|
||||
#
|
||||
@ -312,8 +321,11 @@ stop_bigbluebutton () {
|
||||
if [ -f /usr/lib/systemd/system/bbb-transcode-akka.service ]; then
|
||||
BBB_TRANSCODE_AKKA=bbb-transcode-akka
|
||||
fi
|
||||
if [ -f /usr/share/etherpad-lite/settings.json ]; then
|
||||
ETHERPAD=etherpad
|
||||
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
|
||||
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
|
||||
else
|
||||
/etc/init.d/monit stop
|
||||
|
||||
@ -357,6 +369,21 @@ stop_bigbluebutton () {
|
||||
}
|
||||
|
||||
start_bigbluebutton () {
|
||||
#
|
||||
# Apply any local configuration options (if exists)
|
||||
#
|
||||
if [ -x /etc/bigbluebutton/bbb-conf/apply-config.sh ]; then
|
||||
echo -n "Applying updates in /etc/bigbluebutton/bbb-conf/apply-config.sh: "
|
||||
/etc/bigbluebutton/bbb-conf/apply-config.sh
|
||||
echo
|
||||
fi
|
||||
|
||||
if grep -q "Failure to connect to CORE_DB sofia_reg_external" /opt/freeswitch/var/log/freeswitch/freeswitch.log; then
|
||||
# See: http://docs.bigbluebutton.org/install/install.html#freeswitch-fails-to-bind-to-ipv4
|
||||
echo "Clearing the FreeSWITCH database."
|
||||
rm -rf /opt/freeswitch/var/lib/freeswitch/db/*
|
||||
fi
|
||||
|
||||
echo "Starting BigBlueButton"
|
||||
if command -v systemctl >/dev/null; then
|
||||
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
|
||||
@ -368,8 +395,11 @@ start_bigbluebutton () {
|
||||
if [ -f /usr/lib/systemd/system/bbb-transcode-akka.service ]; then
|
||||
BBB_TRANSCODE_AKKA=bbb-transcode-akka
|
||||
fi
|
||||
if [ -f /usr/share/etherpad-lite/settings.json ]; then
|
||||
ETHERPAD=etherpad
|
||||
fi
|
||||
|
||||
systemctl start red5 $TOMCAT_SERVICE nginx freeswitch $REDIS_SERVICE bbb-apps-akka $BBB_TRANSCODE_AKKA bbb-fsesl-akka bbb-record-core.timer $HTML5 $WEBHOOKS
|
||||
systemctl start red5 $TOMCAT_SERVICE nginx freeswitch $REDIS_SERVICE bbb-apps-akka $BBB_TRANSCODE_AKKA bbb-fsesl-akka bbb-record-core.timer $HTML5 $WEBHOOKS $ETHERPAD
|
||||
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
|
||||
systemctl start mongod
|
||||
sleep 3
|
||||
@ -1526,6 +1556,22 @@ check_state() {
|
||||
echo "#"
|
||||
fi
|
||||
|
||||
if systemctl status freeswitch | grep -q SETSCHEDULER; then
|
||||
echo "# Error: FreeSWITCH failed to start with SETSCHEDULER error, see"
|
||||
echo "#"
|
||||
echo "# http://docs.bigbluebutton.org/install/install.html#freeswitch-fails-to-start-with-a-setscheduler-error"
|
||||
echo "#"
|
||||
fi
|
||||
|
||||
NCPU=`nproc --all`
|
||||
if [ "$NCPU" -lt "4" ]; then
|
||||
echo "# Warning: found only $NCPU cores, whereas this sherver should have (at least) 4 CPU cores"
|
||||
echo "# to run BigBlueButton in production."
|
||||
echo "#"
|
||||
echo "# http://docs.bigbluebutton.org/install/install.html#minimum-server-requirements"
|
||||
echo "#"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
}
|
||||
|
||||
|
@ -355,6 +355,7 @@ if [ $DELETE ]; then
|
||||
echo "deleting: $MEETING_ID"
|
||||
# Remove the status files first to avoid issues with concurrent
|
||||
# recording processing
|
||||
rm -f /var/bigbluebutton/recording/status/ended/$MEETING_ID*
|
||||
rm -f /var/bigbluebutton/recording/status/recorded/$MEETING_ID*
|
||||
rm -f /var/bigbluebutton/recording/status/archived/$MEETING_ID*
|
||||
rm -f /var/bigbluebutton/recording/status/sanity/$MEETING_ID*
|
||||
|
@ -42,23 +42,32 @@
|
||||
<!-- Body -->
|
||||
<div class='container'>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Welcome Message & Login Into Demo -->
|
||||
<div class='row'>
|
||||
<div class='span five'>
|
||||
<h2>Welcome</h2>
|
||||
<p> <a href="http://bigbluebutton.org/" target="_blank">BigBlueButton</a> is an open source web conferencing system for on-line learning. </p>
|
||||
<p> We believe that every student with a web browser should have access to a high-quality on-line learning experience </p>
|
||||
<p> We intend to make that possible with BigBlueButton.</p>
|
||||
<div class='span six html5clientOnly'>
|
||||
<div class='join-meeting '>
|
||||
<h4>Try BigBlueButton via HTML5</h4>
|
||||
<p>New School</p>
|
||||
|
||||
<h4>For Developers</h4>
|
||||
<p> The BigBlueButton project is <a href="http://bigbluebutton.org/support">supported</a> by a community of developers that care about good design and a streamlined user experience. </p>
|
||||
<p>See <a href="/demo/demo1.jsp" target="_blank">API examples </a> for how to integrate BigBlueButton with your project.</p>
|
||||
<form name="form1" method="GET" onsubmit="return checkform(this);" action="/demo/demoHTML5.jsp">
|
||||
<input type="text" id="username" required="" name="username" placeholder="Enter Your Name" size="29" class="field input-default" autofocus>
|
||||
<input type="submit" value="Join" class="submit_btn button success large"><br>
|
||||
<input type="hidden" name="isModerator" value="true">
|
||||
<input type="hidden" name="action" value="create">
|
||||
</form>
|
||||
|
||||
<a class="watch" href="https://www.youtube.com/watch?v=d5v6Uar79Yc" class="pull-right">Overview Video of New HTML5 Client</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="span one"></div>
|
||||
<div class='span six'>
|
||||
<div class='join-meeting pull-right'>
|
||||
<h4>Try BigBlueButton</h4>
|
||||
<p>Join a demo session on this server.</p>
|
||||
<div class='join-meeting'>
|
||||
<h4>Try BigBlueButton via Flash</h4>
|
||||
<p>Old School</p>
|
||||
|
||||
<form name="form1" method="GET" onsubmit="return checkform(this);" action="/demo/demo1.jsp">
|
||||
<input type="text" id="username" required="" name="username" placeholder="Enter Your Name" size="29" class="field input-default" autofocus>
|
||||
@ -66,13 +75,31 @@
|
||||
<input type="hidden" name="action" value="create">
|
||||
</form>
|
||||
|
||||
<a class="watch" href="#video" class="pull-right">New to BigBlueButton? Watch these videos.</a>
|
||||
|
||||
<a class="watch" href="#video" class="pull-right">Tutorial Videos</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="featurette-divider">
|
||||
<!-- Welcome Message & Login Into Demo -->
|
||||
<div class='row'>
|
||||
<div >
|
||||
<h2>Built for online learning</h2>
|
||||
<p> BigBlueButton is an open source web conferencing system built for on-line learning.</p>
|
||||
<p>BigBlueButton provides all the core capabilities you expect in a web conferencing system: real-time sharing of audio, video, presentation, and screen. BigBlueButton gives you many ways to engage users with interactive chat (public and private), multi-user whiteboard, shared notes, emojis, polling, and breakout rooms. You can record any session for later playback.</p>
|
||||
<p> The big change in this release is the new <a href="https://www.youtube.com/watch?v=d5v6Uar79Yc">HTML5 client</a>. The HTML5 client runs on all devices: laptop, desktop, chromebook and mobile (Android 6.0+ and iOS 12.0+). The HTML5 client uses the browser's built-in support for <a href="https://webrtc.org/">web real-time communications (WebRTC)</a> to share audio, video, and screen with other users. Chrome and FireFox are the recommend as they have the best support for WebRTC.</p>
|
||||
<p> Try out the HTML5 client above and give us <a href="https://docs.google.com/forms/d/1gFz5JdN3vD6jxhlVskFYgtEKEcexdDnUzpkwUXwQ4OY/viewform?usp=send_for">your feedback</a>. To learn more about BigBlueButton visit <a href="https://bigbluebutton.org/">bigbluebutton.org</a></p>
|
||||
|
||||
<h4>For Developers</h4>
|
||||
<p> Under the hood, the HTML5 client uses the React for a responsive experience. The BigBlueButton project is <a href="http://bigbluebutton.org/support">supported</a> by a community of developers that care about good design and a streamlined user experience. See <a href="http://docs.bigbluebutton.org" target="_blank">our documentation site</a> for more information on how you can integrate BigBlueButton with your project.</p>
|
||||
</div>
|
||||
<div class="span one"></div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<hr class="featurette-divider">
|
||||
|
||||
<!-- BigBlueButton Features -->
|
||||
@ -217,19 +244,19 @@
|
||||
</div>
|
||||
|
||||
<div class='span four video-item'>
|
||||
<a href="https://www.youtube.com/watch?v=oh0bEk3YSwI" target="_blank">
|
||||
<a href="https://www.youtube.com/watch?v=GeJbK1lvl2I" target="_blank">
|
||||
<div class="video-btn"><i class="fa fa-play-circle-o"></i></div>
|
||||
<img src="images/bbb-viewer-overview.jpg" alt="BigBlueButton Viewer Overview Video"/>
|
||||
</a>
|
||||
<h3><a href="https://www.youtube.com/watch?v=oh0bEk3YSwI;feature=youtu.be" title="Student Overview" target="_blank">Viewer Overview</a></h3>
|
||||
<h3><a href="https://www.youtube.com/watch?v=GeJbK1lvl2I;feature=youtu.be" title="Student Overview" target="_blank">Viewer Overview</a></h3>
|
||||
</div>
|
||||
|
||||
<div class='span four last video-item'>
|
||||
<a href="https://www.youtube.com/watch?v=J9mbw00P9W0&feature=youtu.be" target="_blank">
|
||||
<a href="https://www.youtube.com/watch?v=758xaFdeoN0&feature=youtu.be" target="_blank">
|
||||
<div class="video-btn"><i class="fa fa-play-circle-o"></i></div>
|
||||
<img src="images/bbb-presenter-overview.jpg" alt="Moderator/Presenter Overview Video"/>
|
||||
</a>
|
||||
<h3><a href="https://www.youtube.com/watch?v=J9mbw00P9W0&feature=youtu.be" title="Moderator/Presenter Overview" target="_blank">Moderator/Presenter Overview</a></h3>
|
||||
<h3><a href="https://www.youtube.com/watch?v=758xaFdeoN0&feature=youtu.be" title="Moderator/Presenter Overview" target="_blank">Moderator/Presenter Overview</a></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -261,8 +288,8 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="span twelve center">
|
||||
<p>Copyright © 2018 BigBlueButton Inc.<br>
|
||||
<small>Version <a href="http://docs.bigbluebutton.org/">2.0-RC2</a></small>
|
||||
<p>Copyright © 2019 BigBlueButton Inc.<br>
|
||||
<small>Version <a href="http://docs.bigbluebutton.org/">2.2-dev</a></small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -270,3 +297,4 @@
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
8490
bigbluebutton-html5/client/compatibility/adapter.js
Normal file → Executable file
8490
bigbluebutton-html5/client/compatibility/adapter.js
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
@ -1,601 +0,0 @@
|
||||
|
||||
var userID, callerIdName=null, conferenceVoiceBridge, userAgent=null, userMicMedia, userWebcamMedia, currentSession=null, callTimeout, callActive, callICEConnected, iceConnectedTimeout, callFailCounter, callPurposefullyEnded, uaConnected, transferTimeout, iceGatheringTimeout;
|
||||
var inEchoTest = true;
|
||||
var html5StunTurn = null;
|
||||
|
||||
function webRTCCallback(message) {
|
||||
switch (message.status) {
|
||||
case 'succeded':
|
||||
BBB.webRTCCallSucceeded();
|
||||
break;
|
||||
case 'failed':
|
||||
if (message.errorcode !== 1004) {
|
||||
message.cause = null;
|
||||
}
|
||||
monitorTracksStop();
|
||||
BBB.webRTCCallFailed(inEchoTest, message.errorcode, message.cause);
|
||||
break;
|
||||
case 'ended':
|
||||
monitorTracksStop();
|
||||
BBB.webRTCCallEnded(inEchoTest);
|
||||
break;
|
||||
case 'started':
|
||||
monitorTracksStart();
|
||||
BBB.webRTCCallStarted(inEchoTest);
|
||||
break;
|
||||
case 'connecting':
|
||||
BBB.webRTCCallConnecting(inEchoTest);
|
||||
break;
|
||||
case 'waitingforice':
|
||||
BBB.webRTCCallWaitingForICE(inEchoTest);
|
||||
break;
|
||||
case 'transferring':
|
||||
BBB.webRTCCallTransferring(inEchoTest);
|
||||
break;
|
||||
case 'mediarequest':
|
||||
BBB.webRTCMediaRequest();
|
||||
break;
|
||||
case 'mediasuccess':
|
||||
BBB.webRTCMediaSuccess();
|
||||
break;
|
||||
case 'mediafail':
|
||||
BBB.webRTCMediaFail();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function callIntoConference(voiceBridge, callback, isListenOnly, stunTurn = null) {
|
||||
// root of the call initiation process from the html5 client
|
||||
// Flash will not pass in the listen only field. For html5 it is optional. Assume NOT listen only if no state passed
|
||||
if (isListenOnly == null) {
|
||||
isListenOnly = false;
|
||||
}
|
||||
|
||||
// if additional stun configuration is passed, store the information
|
||||
if (stunTurn != null) {
|
||||
html5StunTurn = {
|
||||
stunServers: stunTurn.stun,
|
||||
turnServers: stunTurn.turn,
|
||||
};
|
||||
}
|
||||
|
||||
// reset callerIdName
|
||||
callerIdName = null;
|
||||
if (!callerIdName) {
|
||||
BBB.getMyUserInfo(function(userInfo) {
|
||||
console.log("User info callback [myUserID=" + userInfo.myUserID
|
||||
+ ",myUsername=" + userInfo.myUsername + ",myAvatarURL=" + userInfo.myAvatarURL
|
||||
+ ",myRole=" + userInfo.myRole + ",amIPresenter=" + userInfo.amIPresenter
|
||||
+ ",dialNumber=" + userInfo.dialNumber + ",voiceBridge=" + userInfo.voiceBridge
|
||||
+ ",isListenOnly=" + isListenOnly + "].");
|
||||
userID = userInfo.myUserID;
|
||||
callerIdName = userInfo.myUserID + "-bbbID-" + userInfo.myUsername;
|
||||
if (isListenOnly) {
|
||||
//prepend the callerIdName so it is recognized as a global audio user
|
||||
callerIdName = "GLOBAL_AUDIO_" + callerIdName;
|
||||
}
|
||||
conferenceVoiceBridge = userInfo.voiceBridge
|
||||
if (voiceBridge === "9196") {
|
||||
voiceBridge = voiceBridge + conferenceVoiceBridge;
|
||||
} else {
|
||||
voiceBridge = conferenceVoiceBridge;
|
||||
}
|
||||
console.log(callerIdName);
|
||||
webrtc_call(callerIdName, voiceBridge, callback, isListenOnly);
|
||||
});
|
||||
} else {
|
||||
if (voiceBridge === "9196") {
|
||||
voiceBridge = voiceBridge + conferenceVoiceBridge;
|
||||
} else {
|
||||
voiceBridge = conferenceVoiceBridge;
|
||||
}
|
||||
webrtc_call(callerIdName, voiceBridge, callback, isListenOnly);
|
||||
}
|
||||
}
|
||||
|
||||
function joinWebRTCVoiceConference() {
|
||||
console.log("Joining to the voice conference directly");
|
||||
inEchoTest = false;
|
||||
// set proper callbacks to previously created user agent
|
||||
if(userAgent) {
|
||||
setUserAgentListeners(webRTCCallback);
|
||||
}
|
||||
callIntoConference(conferenceVoiceBridge, webRTCCallback);
|
||||
}
|
||||
|
||||
function leaveWebRTCVoiceConference() {
|
||||
console.log("Leaving the voice conference");
|
||||
|
||||
webrtc_hangup();
|
||||
}
|
||||
|
||||
function startWebRTCAudioTest(){
|
||||
console.log("Joining the audio test first");
|
||||
inEchoTest = true;
|
||||
callIntoConference("9196", webRTCCallback);
|
||||
}
|
||||
|
||||
function stopWebRTCAudioTest(){
|
||||
console.log("Stopping webrtc audio test");
|
||||
|
||||
webrtc_hangup();
|
||||
}
|
||||
|
||||
function stopWebRTCAudioTestJoinConference(){
|
||||
console.log("Transferring from audio test to conference");
|
||||
|
||||
webRTCCallback({'status': 'transferring'});
|
||||
|
||||
transferTimeout = setTimeout( function() {
|
||||
console.log("Call transfer failed. No response after 3 seconds");
|
||||
webRTCCallback({'status': 'failed', 'errorcode': 1008});
|
||||
releaseUserMedia();
|
||||
currentSession = null;
|
||||
if (userAgent != null) {
|
||||
var userAgentTemp = userAgent;
|
||||
userAgent = null;
|
||||
userAgentTemp.stop();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
BBB.listen("UserJoinedVoiceEvent", userJoinedVoiceHandler);
|
||||
|
||||
currentSession.dtmf(1);
|
||||
inEchoTest = false;
|
||||
}
|
||||
|
||||
function userJoinedVoiceHandler(event) {
|
||||
console.log("UserJoinedVoiceHandler - " + event);
|
||||
if (inEchoTest === false && userID === event.userID) {
|
||||
BBB.unlisten("UserJoinedVoiceEvent", userJoinedVoiceHandler);
|
||||
clearTimeout(transferTimeout);
|
||||
webRTCCallback({'status': 'started'});
|
||||
}
|
||||
}
|
||||
|
||||
function createUA(username, server, callback, makeCallFunc) {
|
||||
if (userAgent) {
|
||||
console.log("User agent already created");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Fetching STUN/TURN server info for user agent");
|
||||
|
||||
console.log(html5StunTurn);
|
||||
if (html5StunTurn != null) {
|
||||
createUAWithStuns(username, server, callback, html5StunTurn, makeCallFunc);
|
||||
return;
|
||||
}
|
||||
|
||||
BBB.getSessionToken(function(sessionToken) {
|
||||
$.ajax({
|
||||
dataType: 'json',
|
||||
url: '/bigbluebutton/api/stuns',
|
||||
data: {sessionToken:sessionToken}
|
||||
}).done(function(data) {
|
||||
var stunsConfig = {};
|
||||
stunsConfig['stunServers'] = ( data['stunServers'] ? data['stunServers'].map(function(data) {
|
||||
return data['url'];
|
||||
}) : [] );
|
||||
stunsConfig['turnServers'] = ( data['turnServers'] ? data['turnServers'].map(function(data) {
|
||||
return {
|
||||
'urls': data['url'],
|
||||
'username': data['username'],
|
||||
'password': data['password']
|
||||
};
|
||||
}) : [] );
|
||||
//stunsConfig['remoteIceCandidates'] = ( data['remoteIceCandidates'] ? data['remoteIceCandidates'].map(function(data) {
|
||||
// return data['ip'];
|
||||
//}) : [] );
|
||||
createUAWithStuns(username, server, callback, stunsConfig, makeCallFunc);
|
||||
}).fail(function(data, textStatus, errorThrown) {
|
||||
BBBLog.error("Could not fetch stun/turn servers", {error: textStatus, user: callerIdName, voiceBridge: conferenceVoiceBridge});
|
||||
callback({'status':'failed', 'errorcode': 1009});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createUAWithStuns(username, server, callback, stunsConfig, makeCallFunc) {
|
||||
console.log("Creating new user agent");
|
||||
|
||||
/* VERY IMPORTANT
|
||||
* - You must escape the username because spaces will cause the connection to fail
|
||||
* - We are connecting to the websocket through an nginx redirect instead of directly to 5066
|
||||
*/
|
||||
var configuration = {
|
||||
uri: 'sip:' + encodeURIComponent(username) + '@' + server,
|
||||
wsServers: ('https:' == document.location.protocol ? 'wss://' : 'ws://') + server + '/ws',
|
||||
displayName: username,
|
||||
register: false,
|
||||
traceSip: true,
|
||||
autostart: false,
|
||||
userAgentString: "BigBlueButton",
|
||||
stunServers: stunsConfig['stunServers'],
|
||||
turnServers: stunsConfig['turnServers'],
|
||||
//artificialRemoteIceCandidates: stunsConfig['remoteIceCandidates']
|
||||
};
|
||||
|
||||
uaConnected = false;
|
||||
|
||||
userAgent = new SIP.UA(configuration);
|
||||
setUserAgentListeners(callback, makeCallFunc);
|
||||
userAgent.start();
|
||||
};
|
||||
|
||||
function setUserAgentListeners(callback, makeCallFunc) {
|
||||
console.log("resetting UA callbacks");
|
||||
userAgent.removeAllListeners('connected');
|
||||
userAgent.on('connected', function() {
|
||||
uaConnected = true;
|
||||
callback({'status':'succeded'});
|
||||
makeCallFunc();
|
||||
});
|
||||
userAgent.removeAllListeners('disconnected');
|
||||
userAgent.on('disconnected', function() {
|
||||
if (userAgent) {
|
||||
if (userAgent != null) {
|
||||
var userAgentTemp = userAgent;
|
||||
userAgent = null;
|
||||
userAgentTemp.stop();
|
||||
}
|
||||
|
||||
if (uaConnected) {
|
||||
callback({'status':'failed', 'errorcode': 1001}); // WebSocket disconnected
|
||||
} else {
|
||||
callback({'status':'failed', 'errorcode': 1002}); // Could not make a WebSocket connection
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function getUserMicMedia(getUserMicMediaSuccess, getUserMicMediaFailure) {
|
||||
if (userMicMedia == undefined) {
|
||||
if (SIP.WebRTC.isSupported()) {
|
||||
SIP.WebRTC.getUserMedia({audio:true, video:false}, getUserMicMediaSuccess, getUserMicMediaFailure);
|
||||
} else {
|
||||
console.log("getUserMicMedia: webrtc not supported");
|
||||
getUserMicMediaFailure("WebRTC is not supported");
|
||||
}
|
||||
} else {
|
||||
console.log("getUserMicMedia: mic already set");
|
||||
getUserMicMediaSuccess(userMicMedia);
|
||||
}
|
||||
};
|
||||
|
||||
function webrtc_call(username, voiceBridge, callback, isListenOnly) {
|
||||
if (!isWebRTCAvailable()) {
|
||||
callback({'status': 'failed', 'errorcode': 1003}); // Browser version not supported
|
||||
return;
|
||||
}
|
||||
if (isListenOnly == null) { // assume NOT listen only unless otherwise stated
|
||||
isListenOnly = false;
|
||||
}
|
||||
|
||||
var server = window.document.location.hostname;
|
||||
console.log("user " + username + " calling to " + voiceBridge);
|
||||
|
||||
var makeCallFunc = function() {
|
||||
// only make the call when both microphone and useragent have been created
|
||||
// for listen only, stating listen only is a viable substitute for acquiring user media control
|
||||
if ((isListenOnly||userMicMedia) && userAgent)
|
||||
make_call(username, voiceBridge, server, callback, false, isListenOnly);
|
||||
};
|
||||
|
||||
// Reset userAgent so we can successfully switch between listenOnly and listen+speak modes
|
||||
userAgent = null;
|
||||
if (!userAgent) {
|
||||
createUA(username, server, callback, makeCallFunc);
|
||||
}
|
||||
// if the user requests to proceed as listen only (does not require media) or media is already acquired,
|
||||
// proceed with making the call
|
||||
if (isListenOnly || userMicMedia != null) {
|
||||
makeCallFunc();
|
||||
} else {
|
||||
callback({'status':'mediarequest'});
|
||||
getUserMicMedia(function(stream) {
|
||||
console.log("getUserMicMedia: success");
|
||||
userMicMedia = stream;
|
||||
callback({'status':'mediasuccess'});
|
||||
makeCallFunc();
|
||||
}, function(e) {
|
||||
console.error("getUserMicMedia: failure - " + e);
|
||||
callback({'status':'mediafail', 'cause': e});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function make_call(username, voiceBridge, server, callback, recall, isListenOnly) {
|
||||
if (isListenOnly == null) {
|
||||
isListenOnly = false;
|
||||
}
|
||||
|
||||
if (userAgent == null) {
|
||||
console.log("userAgent is still null. Delaying call");
|
||||
var callDelayTimeout = setTimeout( function() {
|
||||
make_call(username, voiceBridge, server, callback, recall, isListenOnly);
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userAgent.isConnected()) {
|
||||
console.log("Trying to make call, but UserAgent hasn't connected yet. Delaying call");
|
||||
userAgent.once('connected', function() {
|
||||
console.log("UserAgent has now connected, retrying the call");
|
||||
make_call(username, voiceBridge, server, callback, recall, isListenOnly);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSession) {
|
||||
console.log('Active call detected ignoring second make_call');
|
||||
return;
|
||||
}
|
||||
|
||||
// Make an audio/video call:
|
||||
console.log("Setting options.. ");
|
||||
|
||||
var options = {};
|
||||
if (isListenOnly) {
|
||||
// create necessary options for a listen only stream
|
||||
var stream = null;
|
||||
// handle the web browser
|
||||
// create a stream object through the browser separated from user media
|
||||
if (typeof webkitMediaStream !== 'undefined') {
|
||||
// Google Chrome
|
||||
stream = new webkitMediaStream;
|
||||
} else {
|
||||
// Firefox
|
||||
audioContext = new window.AudioContext;
|
||||
stream = audioContext.createMediaStreamDestination().stream;
|
||||
}
|
||||
|
||||
options = {
|
||||
media: {
|
||||
stream: stream, // use the stream created above
|
||||
constraints: {
|
||||
audio: true,
|
||||
video: false
|
||||
},
|
||||
render: {
|
||||
remote: document.getElementById('remote-media')
|
||||
}
|
||||
},
|
||||
// a list of our RTC Connection constraints
|
||||
RTCConstraints: {
|
||||
// our constraints are mandatory. We must received audio and must not receive audio
|
||||
mandatory: {
|
||||
OfferToReceiveAudio: true,
|
||||
OfferToReceiveVideo: false
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
options = {
|
||||
media: {
|
||||
stream: userMicMedia,
|
||||
constraints: {
|
||||
audio: true,
|
||||
video: false
|
||||
},
|
||||
render: {
|
||||
remote: document.getElementById('remote-media')
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
callTimeout = setTimeout(function() {
|
||||
console.log('Ten seconds without updates sending timeout code');
|
||||
callback({'status':'failed', 'errorcode': 1006}); // Failure on call
|
||||
releaseUserMedia();
|
||||
currentSession = null;
|
||||
if (userAgent != null) {
|
||||
var userAgentTemp = userAgent;
|
||||
userAgent = null;
|
||||
userAgentTemp.stop();
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
callActive = false;
|
||||
callICEConnected = false;
|
||||
callPurposefullyEnded = false;
|
||||
callFailCounter = 0;
|
||||
console.log("Calling to " + voiceBridge + "....");
|
||||
currentSession = userAgent.invite('sip:' + voiceBridge + '@' + server, options);
|
||||
|
||||
// Only send the callback if it's the first try
|
||||
if (recall === false) {
|
||||
console.log('call connecting');
|
||||
callback({'status':'connecting'});
|
||||
} else {
|
||||
console.log('call connecting again');
|
||||
}
|
||||
|
||||
/*
|
||||
iceGatheringTimeout = setTimeout(function() {
|
||||
console.log('Thirty seconds without ICE gathering finishing');
|
||||
callback({'status':'failed', 'errorcode': 1011}); // ICE Gathering Failed
|
||||
releaseUserMedia();
|
||||
currentSession = null;
|
||||
if (userAgent != null) {
|
||||
var userAgentTemp = userAgent;
|
||||
userAgent = null;
|
||||
userAgentTemp.stop();
|
||||
}
|
||||
}, 30000);
|
||||
*/
|
||||
|
||||
currentSession.mediaHandler.on('iceGatheringComplete', function() {
|
||||
clearTimeout(iceGatheringTimeout);
|
||||
});
|
||||
|
||||
// The connecting event fires before the listener can be added
|
||||
currentSession.on('connecting', function(){
|
||||
clearTimeout(callTimeout);
|
||||
});
|
||||
currentSession.on('progress', function(response){
|
||||
console.log('call progress: ' + response);
|
||||
clearTimeout(callTimeout);
|
||||
});
|
||||
currentSession.on('failed', function(response, cause){
|
||||
console.log('call failed with cause: '+ cause);
|
||||
|
||||
if (currentSession) {
|
||||
releaseUserMedia();
|
||||
if (callActive === false) {
|
||||
callback({'status':'failed', 'errorcode': 1004, 'cause': cause}); // Failure on call
|
||||
currentSession = null;
|
||||
if (userAgent != null) {
|
||||
var userAgentTemp = userAgent;
|
||||
userAgent = null;
|
||||
userAgentTemp.stop();
|
||||
}
|
||||
} else {
|
||||
callActive = false;
|
||||
//currentSession.bye();
|
||||
currentSession = null;
|
||||
if (userAgent != null) {
|
||||
userAgent.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
clearTimeout(callTimeout);
|
||||
});
|
||||
currentSession.on('bye', function(request){
|
||||
callActive = false;
|
||||
|
||||
if (currentSession) {
|
||||
console.log('call ended ' + currentSession.endTime);
|
||||
releaseUserMedia();
|
||||
if (callPurposefullyEnded === true) {
|
||||
callback({'status':'ended'});
|
||||
} else {
|
||||
callback({'status':'failed', 'errorcode': 1005}); // Call ended unexpectedly
|
||||
}
|
||||
clearTimeout(callTimeout);
|
||||
currentSession = null;
|
||||
} else {
|
||||
console.log('bye event already received');
|
||||
}
|
||||
});
|
||||
currentSession.on('cancel', function(request) {
|
||||
callActive = false;
|
||||
|
||||
if (currentSession) {
|
||||
console.log('call canceled');
|
||||
releaseUserMedia();
|
||||
clearTimeout(callTimeout);
|
||||
currentSession = null;
|
||||
} else {
|
||||
console.log('cancel event already received');
|
||||
}
|
||||
});
|
||||
currentSession.on('accepted', function(data){
|
||||
callActive = true;
|
||||
console.log('BigBlueButton call accepted');
|
||||
|
||||
if (callICEConnected === true) {
|
||||
callback({'status':'started'});
|
||||
} else {
|
||||
callback({'status':'waitingforice'});
|
||||
console.log('Waiting for ICE negotiation');
|
||||
iceConnectedTimeout = setTimeout(function() {
|
||||
console.log('5 seconds without ICE finishing');
|
||||
callback({'status':'failed', 'errorcode': 1010}); // ICE negotiation timeout
|
||||
releaseUserMedia();
|
||||
currentSession = null;
|
||||
if (userAgent != null) {
|
||||
var userAgentTemp = userAgent;
|
||||
userAgent = null;
|
||||
userAgentTemp.stop();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
clearTimeout(callTimeout);
|
||||
});
|
||||
currentSession.mediaHandler.on('iceConnectionFailed', function() {
|
||||
console.log('received ice negotiation failed');
|
||||
callback({'status':'failed', 'errorcode': 1007}); // Failure on call
|
||||
releaseUserMedia();
|
||||
currentSession = null;
|
||||
clearTimeout(iceConnectedTimeout);
|
||||
if (userAgent != null) {
|
||||
var userAgentTemp = userAgent;
|
||||
userAgent = null;
|
||||
userAgentTemp.stop();
|
||||
}
|
||||
|
||||
clearTimeout(callTimeout);
|
||||
});
|
||||
|
||||
// Some browsers use status of 'connected', others use 'completed', and a couple use both
|
||||
|
||||
currentSession.mediaHandler.on('iceConnectionConnected', function() {
|
||||
console.log('Received ICE status changed to connected');
|
||||
if (callICEConnected === false) {
|
||||
callICEConnected = true;
|
||||
clearTimeout(iceConnectedTimeout);
|
||||
if (callActive === true) {
|
||||
callback({'status':'started'});
|
||||
}
|
||||
clearTimeout(callTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
currentSession.mediaHandler.on('iceConnectionCompleted', function() {
|
||||
console.log('Received ICE status changed to completed');
|
||||
if (callICEConnected === false) {
|
||||
callICEConnected = true;
|
||||
clearTimeout(iceConnectedTimeout);
|
||||
if (callActive === true) {
|
||||
callback({'status':'started'});
|
||||
}
|
||||
clearTimeout(callTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function webrtc_hangup(callback) {
|
||||
callPurposefullyEnded = true;
|
||||
|
||||
console.log("Hanging up current session");
|
||||
if (callback) {
|
||||
currentSession.on('bye', callback);
|
||||
}
|
||||
try {
|
||||
currentSession.bye();
|
||||
} catch (err) {
|
||||
console.log("Forcing to cancel current session");
|
||||
currentSession.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
function releaseUserMedia() {
|
||||
if (!!userMicMedia) {
|
||||
console.log("Releasing media tracks");
|
||||
|
||||
userMicMedia.getAudioTracks().forEach(function(track) {
|
||||
track.stop();
|
||||
});
|
||||
|
||||
userMicMedia.getVideoTracks().forEach(function(track) {
|
||||
track.stop();
|
||||
});
|
||||
|
||||
userMicMedia = null;
|
||||
}
|
||||
}
|
||||
|
||||
function isWebRTCAvailable() {
|
||||
if (bowser.msedge) {
|
||||
return false;
|
||||
} else {
|
||||
return SIP.WebRTC.isSupported();
|
||||
}
|
||||
}
|
||||
|
||||
function getCallStatus() {
|
||||
return currentSession;
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ const isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
|
||||
const isChrome = !!window.chrome && !isOpera;
|
||||
const isSafari = navigator.userAgent.indexOf('Safari') >= 0 && !isChrome;
|
||||
const isElectron = navigator.userAgent.toLowerCase().indexOf(' electron/') > -1;
|
||||
const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function'
|
||||
|| (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function'));
|
||||
const kurentoHandler = null;
|
||||
|
||||
Kurento = function (
|
||||
@ -620,7 +622,7 @@ Kurento.normalizeCallback = function (callback) {
|
||||
|
||||
// this function explains how to use above methods/objects
|
||||
window.getScreenConstraints = function (sendSource, callback) {
|
||||
const screenConstraints = { video: {}, audio: false };
|
||||
let screenConstraints = { video: {}, audio: false };
|
||||
|
||||
// Limiting FPS to a range of 5-10 (5 ideal)
|
||||
screenConstraints.video.frameRate = { ideal: 5, max: 10 };
|
||||
@ -643,6 +645,23 @@ window.getScreenConstraints = function (sendSource, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
const getDisplayMediaConstraints = function () {
|
||||
// The fine-grained constraints (e.g.: frameRate) are supposed to go into
|
||||
// the MediaStream because getDisplayMedia does not support them,
|
||||
// so they're passed differently
|
||||
kurentoManager.kurentoScreenshare.extensionInstalled = true;
|
||||
optionalConstraints.width = { max: kurentoManager.kurentoScreenshare.vid_max_width };
|
||||
optionalConstraints.height = { max: kurentoManager.kurentoScreenshare.vid_max_height };
|
||||
optionalConstraints.frameRate = { ideal: 5, max: 10 };
|
||||
|
||||
let gDPConstraints = {
|
||||
video: true,
|
||||
optional: optionalConstraints
|
||||
}
|
||||
|
||||
return gDPConstraints;
|
||||
};
|
||||
|
||||
const optionalConstraints = [
|
||||
{ googCpuOveruseDetection: true },
|
||||
{ googCpuOveruseEncodeUsage: true },
|
||||
@ -669,25 +688,34 @@ window.getScreenConstraints = function (sendSource, callback) {
|
||||
}
|
||||
|
||||
if (isChrome) {
|
||||
const extensionKey = kurentoManager.getChromeExtensionKey();
|
||||
getChromeScreenConstraints(extensionKey).then((constraints) => {
|
||||
if (!constraints) {
|
||||
document.dispatchEvent(new Event('installChromeExtension'));
|
||||
return;
|
||||
}
|
||||
if (!hasDisplayMedia) {
|
||||
const extensionKey = kurentoManager.getChromeExtensionKey();
|
||||
getChromeScreenConstraints(extensionKey).then((constraints) => {
|
||||
if (!constraints) {
|
||||
document.dispatchEvent(new Event('installChromeExtension'));
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceId = constraints.streamId;
|
||||
const sourceId = constraints.streamId;
|
||||
|
||||
kurentoManager.kurentoScreenshare.extensionInstalled = true;
|
||||
kurentoManager.kurentoScreenshare.extensionInstalled = true;
|
||||
|
||||
// this statement sets gets 'sourceId" and sets "chromeMediaSourceId"
|
||||
screenConstraints.video.chromeMediaSource = { exact: [sendSource] };
|
||||
screenConstraints.video.chromeMediaSourceId = sourceId;
|
||||
screenConstraints.optional = optionalConstraints;
|
||||
// Re-wrap the video constraints into the mandatory object (latest adapter)
|
||||
screenConstraints.video = {}
|
||||
screenConstraints.video.mandatory = {};
|
||||
screenConstraints.video.mandatory.maxFrameRate = 10;
|
||||
screenConstraints.video.mandatory.maxHeight = kurentoManager.kurentoScreenshare.vid_max_height;
|
||||
screenConstraints.video.mandatory.maxWidth = kurentoManager.kurentoScreenshare.vid_max_width;
|
||||
screenConstraints.video.mandatory.chromeMediaSource = sendSource;
|
||||
screenConstraints.video.mandatory.chromeMediaSourceId = sourceId;
|
||||
screenConstraints.optional = optionalConstraints;
|
||||
|
||||
console.log('getScreenConstraints for Chrome returns => ', screenConstraints);
|
||||
return callback(null, screenConstraints);
|
||||
});
|
||||
console.log('getScreenConstraints for Chrome returns => ', screenConstraints);
|
||||
return callback(null, screenConstraints);
|
||||
});
|
||||
} else {
|
||||
return callback(null, getDisplayMediaConstraints());
|
||||
}
|
||||
}
|
||||
|
||||
if (isFirefox) {
|
||||
@ -698,9 +726,14 @@ window.getScreenConstraints = function (sendSource, callback) {
|
||||
return callback(null, screenConstraints);
|
||||
}
|
||||
|
||||
// Falls back to getDisplayMedia if the browser supports it
|
||||
if (hasDisplayMedia) {
|
||||
return callback(null, getDisplayMediaConstraints());
|
||||
}
|
||||
|
||||
if (isSafari) {
|
||||
// At this time (version 11.1), Safari doesn't support screenshare.
|
||||
document.dispatchEvent(new Event('safariScreenshareNotSupported'));
|
||||
return document.dispatchEvent(new Event('safariScreenshareNotSupported'));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -193,6 +193,15 @@ function WebRtcPeer(mode, options, callback) {
|
||||
});
|
||||
if (!pc) {
|
||||
pc = new RTCPeerConnection(configuration);
|
||||
|
||||
//Add Transceiver for Webview on IOS
|
||||
const userAgent = window.navigator.userAgent.toLocaleLowerCase();
|
||||
if ((userAgent.indexOf('iphone') > -1 || userAgent.indexOf('ipad') > -1) && userAgent.indexOf('safari') == -1) {
|
||||
try {
|
||||
pc.addTransceiver('video');
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
if (useDataChannels && !dataChannel) {
|
||||
var dcId = 'WebRtcPeer-' + self.id;
|
||||
var dcOptions = undefined;
|
||||
@ -258,6 +267,28 @@ function WebRtcPeer(mode, options, callback) {
|
||||
};
|
||||
this.generateOffer = function (callback) {
|
||||
callback = callback.bind(this);
|
||||
const descriptionCallback = () => {
|
||||
var localDescription = pc.localDescription;
|
||||
// logger.debug('Local description set', localDescription.sdp);
|
||||
if (multistream && usePlanB) {
|
||||
localDescription = interop.toUnifiedPlan(localDescription);
|
||||
logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
|
||||
}
|
||||
callback(null, localDescription.sdp, self.processAnswer.bind(self));
|
||||
}
|
||||
|
||||
const userAgent = window.navigator.userAgent.toLocaleLowerCase();
|
||||
const isSafari = ((userAgent.indexOf('iphone') > -1 || userAgent.indexOf('ipad') > -1) || browser.name.toLowerCase() == 'safari');
|
||||
|
||||
// Bind the SDP release to the gathering state on Safari-based envs
|
||||
if (isSafari) {
|
||||
pc.onicegatheringstatechange = function (event) {
|
||||
if(event.target.iceGatheringState == "complete") {
|
||||
descriptionCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var offerAudio = true;
|
||||
var offerVideo = true;
|
||||
if (mediaConstraints) {
|
||||
@ -274,14 +305,12 @@ function WebRtcPeer(mode, options, callback) {
|
||||
// logger.debug('Created SDP offer');
|
||||
offer = mangleSdpToAddSimulcast(offer);
|
||||
return pc.setLocalDescription(offer);
|
||||
}).then(function () {
|
||||
var localDescription = pc.localDescription;
|
||||
// logger.debug('Local description set', localDescription.sdp);
|
||||
if (multistream && usePlanB) {
|
||||
localDescription = interop.toUnifiedPlan(localDescription);
|
||||
logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
|
||||
}).then(() => {
|
||||
// The Safari offer release was already binded to the gathering state
|
||||
if (isSafari) {
|
||||
return;
|
||||
}
|
||||
callback(null, localDescription.sdp, self.processAnswer.bind(self));
|
||||
descriptionCallback();
|
||||
}).catch(callback);
|
||||
};
|
||||
this.getLocalSessionDescriptor = function () {
|
||||
@ -432,7 +461,27 @@ function WebRtcPeer(mode, options, callback) {
|
||||
return callback(error);
|
||||
constraints = [mediaConstraints];
|
||||
constraints.unshift(constraints_);
|
||||
getMedia(recursive.apply(undefined, constraints));
|
||||
let gDMCallback = function(stream) {
|
||||
stream.getTracks()[0].applyConstraints(constraints[0].optional)
|
||||
.then(() => {
|
||||
videoStream = stream;
|
||||
start();
|
||||
}).catch(() => {
|
||||
videoStream = stream;
|
||||
start();
|
||||
});
|
||||
}
|
||||
if (typeof navigator.getDisplayMedia === 'function') {
|
||||
navigator.getDisplayMedia(recursive.apply(undefined, constraints))
|
||||
.then(gDMCallback)
|
||||
.catch(callback);
|
||||
} else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
|
||||
navigator.mediaDevices.getDisplayMedia(recursive.apply(undefined, constraints))
|
||||
.then(gDMCallback)
|
||||
.catch(callback);
|
||||
} else {
|
||||
getMedia(recursive.apply(undefined, constraints));
|
||||
}
|
||||
}, guid);
|
||||
}
|
||||
} else {
|
||||
@ -1194,180 +1243,180 @@ if (typeof Object.create === 'function') {
|
||||
// Does nothing at all.
|
||||
|
||||
},{}],11:[function(require,module,exports){
|
||||
/*!
|
||||
* @name JavaScript/NodeJS Merge v1.2.0
|
||||
* @author yeikos
|
||||
* @repository https://github.com/yeikos/js.merge
|
||||
|
||||
* Copyright 2014 yeikos - MIT license
|
||||
* https://raw.github.com/yeikos/js.merge/master/LICENSE
|
||||
*/
|
||||
|
||||
;(function(isNode) {
|
||||
|
||||
/**
|
||||
* Merge one or more objects
|
||||
* @param bool? clone
|
||||
* @param mixed,... arguments
|
||||
* @return object
|
||||
*/
|
||||
|
||||
var Public = function(clone) {
|
||||
|
||||
return merge(clone === true, false, arguments);
|
||||
|
||||
}, publicName = 'merge';
|
||||
|
||||
/**
|
||||
* Merge two or more objects recursively
|
||||
* @param bool? clone
|
||||
* @param mixed,... arguments
|
||||
* @return object
|
||||
*/
|
||||
|
||||
Public.recursive = function(clone) {
|
||||
|
||||
return merge(clone === true, true, arguments);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Clone the input removing any reference
|
||||
* @param mixed input
|
||||
* @return mixed
|
||||
*/
|
||||
|
||||
Public.clone = function(input) {
|
||||
|
||||
var output = input,
|
||||
type = typeOf(input),
|
||||
index, size;
|
||||
|
||||
if (type === 'array') {
|
||||
|
||||
output = [];
|
||||
size = input.length;
|
||||
|
||||
for (index=0;index<size;++index)
|
||||
|
||||
output[index] = Public.clone(input[index]);
|
||||
|
||||
} else if (type === 'object') {
|
||||
|
||||
output = {};
|
||||
|
||||
for (index in input)
|
||||
|
||||
output[index] = Public.clone(input[index]);
|
||||
|
||||
}
|
||||
|
||||
return output;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge two objects recursively
|
||||
* @param mixed input
|
||||
* @param mixed extend
|
||||
* @return mixed
|
||||
*/
|
||||
|
||||
function merge_recursive(base, extend) {
|
||||
|
||||
if (typeOf(base) !== 'object')
|
||||
|
||||
return extend;
|
||||
|
||||
for (var key in extend) {
|
||||
|
||||
if (typeOf(base[key]) === 'object' && typeOf(extend[key]) === 'object') {
|
||||
|
||||
base[key] = merge_recursive(base[key], extend[key]);
|
||||
|
||||
} else {
|
||||
|
||||
base[key] = extend[key];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return base;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two or more objects
|
||||
* @param bool clone
|
||||
* @param bool recursive
|
||||
* @param array argv
|
||||
* @return object
|
||||
*/
|
||||
|
||||
function merge(clone, recursive, argv) {
|
||||
|
||||
var result = argv[0],
|
||||
size = argv.length;
|
||||
|
||||
if (clone || typeOf(result) !== 'object')
|
||||
|
||||
result = {};
|
||||
|
||||
for (var index=0;index<size;++index) {
|
||||
|
||||
var item = argv[index],
|
||||
|
||||
type = typeOf(item);
|
||||
|
||||
if (type !== 'object') continue;
|
||||
|
||||
for (var key in item) {
|
||||
|
||||
var sitem = clone ? Public.clone(item[key]) : item[key];
|
||||
|
||||
if (recursive) {
|
||||
|
||||
result[key] = merge_recursive(result[key], sitem);
|
||||
|
||||
} else {
|
||||
|
||||
result[key] = sitem;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type of variable
|
||||
* @param mixed input
|
||||
* @return string
|
||||
*
|
||||
* @see http://jsperf.com/typeofvar
|
||||
*/
|
||||
|
||||
function typeOf(input) {
|
||||
|
||||
return ({}).toString.call(input).slice(8, -1).toLowerCase();
|
||||
|
||||
}
|
||||
|
||||
if (isNode) {
|
||||
|
||||
module.exports = Public;
|
||||
|
||||
} else {
|
||||
|
||||
window[publicName] = Public;
|
||||
|
||||
}
|
||||
|
||||
/*!
|
||||
* @name JavaScript/NodeJS Merge v1.2.0
|
||||
* @author yeikos
|
||||
* @repository https://github.com/yeikos/js.merge
|
||||
|
||||
* Copyright 2014 yeikos - MIT license
|
||||
* https://raw.github.com/yeikos/js.merge/master/LICENSE
|
||||
*/
|
||||
|
||||
;(function(isNode) {
|
||||
|
||||
/**
|
||||
* Merge one or more objects
|
||||
* @param bool? clone
|
||||
* @param mixed,... arguments
|
||||
* @return object
|
||||
*/
|
||||
|
||||
var Public = function(clone) {
|
||||
|
||||
return merge(clone === true, false, arguments);
|
||||
|
||||
}, publicName = 'merge';
|
||||
|
||||
/**
|
||||
* Merge two or more objects recursively
|
||||
* @param bool? clone
|
||||
* @param mixed,... arguments
|
||||
* @return object
|
||||
*/
|
||||
|
||||
Public.recursive = function(clone) {
|
||||
|
||||
return merge(clone === true, true, arguments);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Clone the input removing any reference
|
||||
* @param mixed input
|
||||
* @return mixed
|
||||
*/
|
||||
|
||||
Public.clone = function(input) {
|
||||
|
||||
var output = input,
|
||||
type = typeOf(input),
|
||||
index, size;
|
||||
|
||||
if (type === 'array') {
|
||||
|
||||
output = [];
|
||||
size = input.length;
|
||||
|
||||
for (index=0;index<size;++index)
|
||||
|
||||
output[index] = Public.clone(input[index]);
|
||||
|
||||
} else if (type === 'object') {
|
||||
|
||||
output = {};
|
||||
|
||||
for (index in input)
|
||||
|
||||
output[index] = Public.clone(input[index]);
|
||||
|
||||
}
|
||||
|
||||
return output;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge two objects recursively
|
||||
* @param mixed input
|
||||
* @param mixed extend
|
||||
* @return mixed
|
||||
*/
|
||||
|
||||
function merge_recursive(base, extend) {
|
||||
|
||||
if (typeOf(base) !== 'object')
|
||||
|
||||
return extend;
|
||||
|
||||
for (var key in extend) {
|
||||
|
||||
if (typeOf(base[key]) === 'object' && typeOf(extend[key]) === 'object') {
|
||||
|
||||
base[key] = merge_recursive(base[key], extend[key]);
|
||||
|
||||
} else {
|
||||
|
||||
base[key] = extend[key];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return base;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two or more objects
|
||||
* @param bool clone
|
||||
* @param bool recursive
|
||||
* @param array argv
|
||||
* @return object
|
||||
*/
|
||||
|
||||
function merge(clone, recursive, argv) {
|
||||
|
||||
var result = argv[0],
|
||||
size = argv.length;
|
||||
|
||||
if (clone || typeOf(result) !== 'object')
|
||||
|
||||
result = {};
|
||||
|
||||
for (var index=0;index<size;++index) {
|
||||
|
||||
var item = argv[index],
|
||||
|
||||
type = typeOf(item);
|
||||
|
||||
if (type !== 'object') continue;
|
||||
|
||||
for (var key in item) {
|
||||
|
||||
var sitem = clone ? Public.clone(item[key]) : item[key];
|
||||
|
||||
if (recursive) {
|
||||
|
||||
result[key] = merge_recursive(result[key], sitem);
|
||||
|
||||
} else {
|
||||
|
||||
result[key] = sitem;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type of variable
|
||||
* @param mixed input
|
||||
* @return string
|
||||
*
|
||||
* @see http://jsperf.com/typeofvar
|
||||
*/
|
||||
|
||||
function typeOf(input) {
|
||||
|
||||
return ({}).toString.call(input).slice(8, -1).toLowerCase();
|
||||
|
||||
}
|
||||
|
||||
if (isNode) {
|
||||
|
||||
module.exports = Public;
|
||||
|
||||
} else {
|
||||
|
||||
window[publicName] = Public;
|
||||
|
||||
}
|
||||
|
||||
})(typeof module === 'object' && module && typeof module.exports === 'object' && module.exports);
|
||||
},{}],12:[function(require,module,exports){
|
||||
/**
|
||||
@ -4265,159 +4314,159 @@ uuid.unparse = unparse;
|
||||
module.exports = uuid;
|
||||
|
||||
},{"./rng":22}],24:[function(require,module,exports){
|
||||
/*
|
||||
WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based
|
||||
on @visionmedia's Emitter from UI Kit.
|
||||
|
||||
Why? I wanted it standalone.
|
||||
|
||||
I also wanted support for wildcard emitters like this:
|
||||
|
||||
emitter.on('*', function (eventName, other, event, payloads) {
|
||||
|
||||
});
|
||||
|
||||
emitter.on('somenamespace*', function (eventName, payloads) {
|
||||
|
||||
});
|
||||
|
||||
Please note that callbacks triggered by wildcard registered events also get
|
||||
the event name as the first argument.
|
||||
*/
|
||||
|
||||
module.exports = WildEmitter;
|
||||
|
||||
function WildEmitter() { }
|
||||
|
||||
WildEmitter.mixin = function (constructor) {
|
||||
var prototype = constructor.prototype || constructor;
|
||||
|
||||
prototype.isWildEmitter= true;
|
||||
|
||||
// Listen on the given `event` with `fn`. Store a group name if present.
|
||||
prototype.on = function (event, groupName, fn) {
|
||||
this.callbacks = this.callbacks || {};
|
||||
var hasGroup = (arguments.length === 3),
|
||||
group = hasGroup ? arguments[1] : undefined,
|
||||
func = hasGroup ? arguments[2] : arguments[1];
|
||||
func._groupName = group;
|
||||
(this.callbacks[event] = this.callbacks[event] || []).push(func);
|
||||
return this;
|
||||
};
|
||||
|
||||
// Adds an `event` listener that will be invoked a single
|
||||
// time then automatically removed.
|
||||
prototype.once = function (event, groupName, fn) {
|
||||
var self = this,
|
||||
hasGroup = (arguments.length === 3),
|
||||
group = hasGroup ? arguments[1] : undefined,
|
||||
func = hasGroup ? arguments[2] : arguments[1];
|
||||
function on() {
|
||||
self.off(event, on);
|
||||
func.apply(this, arguments);
|
||||
}
|
||||
this.on(event, group, on);
|
||||
return this;
|
||||
};
|
||||
|
||||
// Unbinds an entire group
|
||||
prototype.releaseGroup = function (groupName) {
|
||||
this.callbacks = this.callbacks || {};
|
||||
var item, i, len, handlers;
|
||||
for (item in this.callbacks) {
|
||||
handlers = this.callbacks[item];
|
||||
for (i = 0, len = handlers.length; i < len; i++) {
|
||||
if (handlers[i]._groupName === groupName) {
|
||||
//console.log('removing');
|
||||
// remove it and shorten the array we're looping through
|
||||
handlers.splice(i, 1);
|
||||
i--;
|
||||
len--;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
// Remove the given callback for `event` or all
|
||||
// registered callbacks.
|
||||
prototype.off = function (event, fn) {
|
||||
this.callbacks = this.callbacks || {};
|
||||
var callbacks = this.callbacks[event],
|
||||
i;
|
||||
|
||||
if (!callbacks) return this;
|
||||
|
||||
// remove all handlers
|
||||
if (arguments.length === 1) {
|
||||
delete this.callbacks[event];
|
||||
return this;
|
||||
}
|
||||
|
||||
// remove specific handler
|
||||
i = callbacks.indexOf(fn);
|
||||
callbacks.splice(i, 1);
|
||||
if (callbacks.length === 0) {
|
||||
delete this.callbacks[event];
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/// Emit `event` with the given args.
|
||||
// also calls any `*` handlers
|
||||
prototype.emit = function (event) {
|
||||
this.callbacks = this.callbacks || {};
|
||||
var args = [].slice.call(arguments, 1),
|
||||
callbacks = this.callbacks[event],
|
||||
specialCallbacks = this.getWildcardCallbacks(event),
|
||||
i,
|
||||
len,
|
||||
item,
|
||||
listeners;
|
||||
|
||||
if (callbacks) {
|
||||
listeners = callbacks.slice();
|
||||
for (i = 0, len = listeners.length; i < len; ++i) {
|
||||
if (!listeners[i]) {
|
||||
break;
|
||||
}
|
||||
listeners[i].apply(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
if (specialCallbacks) {
|
||||
len = specialCallbacks.length;
|
||||
listeners = specialCallbacks.slice();
|
||||
for (i = 0, len = listeners.length; i < len; ++i) {
|
||||
if (!listeners[i]) {
|
||||
break;
|
||||
}
|
||||
listeners[i].apply(this, [event].concat(args));
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
// Helper for for finding special wildcard event handlers that match the event
|
||||
prototype.getWildcardCallbacks = function (eventName) {
|
||||
this.callbacks = this.callbacks || {};
|
||||
var item,
|
||||
split,
|
||||
result = [];
|
||||
|
||||
for (item in this.callbacks) {
|
||||
split = item.split('*');
|
||||
if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) {
|
||||
result = result.concat(this.callbacks[item]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
WildEmitter.mixin(WildEmitter);
|
||||
/*
|
||||
WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based
|
||||
on @visionmedia's Emitter from UI Kit.
|
||||
|
||||
Why? I wanted it standalone.
|
||||
|
||||
I also wanted support for wildcard emitters like this:
|
||||
|
||||
emitter.on('*', function (eventName, other, event, payloads) {
|
||||
|
||||
});
|
||||
|
||||
emitter.on('somenamespace*', function (eventName, payloads) {
|
||||
|
||||
});
|
||||
|
||||
Please note that callbacks triggered by wildcard registered events also get
|
||||
the event name as the first argument.
|
||||
*/
|
||||
|
||||
module.exports = WildEmitter;
|
||||
|
||||
function WildEmitter() { }
|
||||
|
||||
WildEmitter.mixin = function (constructor) {
|
||||
var prototype = constructor.prototype || constructor;
|
||||
|
||||
prototype.isWildEmitter= true;
|
||||
|
||||
// Listen on the given `event` with `fn`. Store a group name if present.
|
||||
prototype.on = function (event, groupName, fn) {
|
||||
this.callbacks = this.callbacks || {};
|
||||
var hasGroup = (arguments.length === 3),
|
||||
group = hasGroup ? arguments[1] : undefined,
|
||||
func = hasGroup ? arguments[2] : arguments[1];
|
||||
func._groupName = group;
|
||||
(this.callbacks[event] = this.callbacks[event] || []).push(func);
|
||||
return this;
|
||||
};
|
||||
|
||||
// Adds an `event` listener that will be invoked a single
|
||||
// time then automatically removed.
|
||||
prototype.once = function (event, groupName, fn) {
|
||||
var self = this,
|
||||
hasGroup = (arguments.length === 3),
|
||||
group = hasGroup ? arguments[1] : undefined,
|
||||
func = hasGroup ? arguments[2] : arguments[1];
|
||||
function on() {
|
||||
self.off(event, on);
|
||||
func.apply(this, arguments);
|
||||
}
|
||||
this.on(event, group, on);
|
||||
return this;
|
||||
};
|
||||
|
||||
// Unbinds an entire group
|
||||
prototype.releaseGroup = function (groupName) {
|
||||
this.callbacks = this.callbacks || {};
|
||||
var item, i, len, handlers;
|
||||
for (item in this.callbacks) {
|
||||
handlers = this.callbacks[item];
|
||||
for (i = 0, len = handlers.length; i < len; i++) {
|
||||
if (handlers[i]._groupName === groupName) {
|
||||
//console.log('removing');
|
||||
// remove it and shorten the array we're looping through
|
||||
handlers.splice(i, 1);
|
||||
i--;
|
||||
len--;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
// Remove the given callback for `event` or all
|
||||
// registered callbacks.
|
||||
prototype.off = function (event, fn) {
|
||||
this.callbacks = this.callbacks || {};
|
||||
var callbacks = this.callbacks[event],
|
||||
i;
|
||||
|
||||
if (!callbacks) return this;
|
||||
|
||||
// remove all handlers
|
||||
if (arguments.length === 1) {
|
||||
delete this.callbacks[event];
|
||||
return this;
|
||||
}
|
||||
|
||||
// remove specific handler
|
||||
i = callbacks.indexOf(fn);
|
||||
callbacks.splice(i, 1);
|
||||
if (callbacks.length === 0) {
|
||||
delete this.callbacks[event];
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/// Emit `event` with the given args.
|
||||
// also calls any `*` handlers
|
||||
prototype.emit = function (event) {
|
||||
this.callbacks = this.callbacks || {};
|
||||
var args = [].slice.call(arguments, 1),
|
||||
callbacks = this.callbacks[event],
|
||||
specialCallbacks = this.getWildcardCallbacks(event),
|
||||
i,
|
||||
len,
|
||||
item,
|
||||
listeners;
|
||||
|
||||
if (callbacks) {
|
||||
listeners = callbacks.slice();
|
||||
for (i = 0, len = listeners.length; i < len; ++i) {
|
||||
if (!listeners[i]) {
|
||||
break;
|
||||
}
|
||||
listeners[i].apply(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
if (specialCallbacks) {
|
||||
len = specialCallbacks.length;
|
||||
listeners = specialCallbacks.slice();
|
||||
for (i = 0, len = listeners.length; i < len; ++i) {
|
||||
if (!listeners[i]) {
|
||||
break;
|
||||
}
|
||||
listeners[i].apply(this, [event].concat(args));
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
// Helper for for finding special wildcard event handlers that match the event
|
||||
prototype.getWildcardCallbacks = function (eventName) {
|
||||
this.callbacks = this.callbacks || {};
|
||||
var item,
|
||||
split,
|
||||
result = [];
|
||||
|
||||
for (item in this.callbacks) {
|
||||
split = item.split('*');
|
||||
if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) {
|
||||
result = result.concat(this.callbacks[item]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
WildEmitter.mixin(WildEmitter);
|
||||
|
||||
},{}]},{},[2])(2)
|
||||
});
|
||||
|
11
bigbluebutton-html5/client/compatibility/sip.js
Executable file → Normal file
11
bigbluebutton-html5/client/compatibility/sip.js
Executable file → Normal file
@ -11564,6 +11564,13 @@ MediaHandler.prototype = Object.create(SIP.MediaHandler.prototype, {
|
||||
self.ready = false;
|
||||
methodName = self.hasOffer('remote') ? 'createAnswer' : 'createOffer';
|
||||
|
||||
if(constraints.offerToReceiveAudio) {
|
||||
//Needed for Safari on webview
|
||||
try {
|
||||
pc.addTransceiver('audio');
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return SIP.Utils.promisify(pc, methodName, true)(constraints)
|
||||
.catch(function methodError(e) {
|
||||
self.emit('peerConnection-' + methodName + 'Failed', e);
|
||||
@ -11611,7 +11618,9 @@ MediaHandler.prototype = Object.create(SIP.MediaHandler.prototype, {
|
||||
try {
|
||||
streams = [].concat(streams);
|
||||
streams.forEach(function (stream) {
|
||||
this.peerConnection.addStream(stream);
|
||||
try {
|
||||
this.peerConnection.addStream(stream);
|
||||
} catch (e) {}
|
||||
}, this);
|
||||
} catch(e) {
|
||||
this.logger.error('error adding stream');
|
||||
|
@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker';
|
||||
import BaseAudioBridge from './base';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import { fetchStunTurnServers } from '/imports/utils/fetchStunTurnServers';
|
||||
|
||||
import browser from 'browser-detect';
|
||||
|
||||
const MEDIA = Meteor.settings.public.media;
|
||||
const MEDIA_TAG = MEDIA.mediaTag;
|
||||
@ -14,6 +14,13 @@ const CALL_HANGUP_MAX_RETRIES = MEDIA.callHangupMaximumRetries;
|
||||
const CONNECTION_TERMINATED_EVENTS = ['iceConnectionFailed', 'iceConnectionClosed'];
|
||||
const CALL_CONNECT_NOTIFICATION_TIMEOUT = 500;
|
||||
|
||||
const logConnector = (level, category, label, content) => {
|
||||
if (level === 'log')
|
||||
level = "info";
|
||||
|
||||
logger[level]({logCode: 'sipjs_log'}, '[' + category + '] ' + content);
|
||||
};
|
||||
|
||||
export default class SIPBridge extends BaseAudioBridge {
|
||||
constructor(userData) {
|
||||
super(userData);
|
||||
@ -199,6 +206,7 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws`,
|
||||
log: {
|
||||
builtinEnabled: false,
|
||||
connector: logConnector
|
||||
},
|
||||
displayName: callerIdName,
|
||||
register: false,
|
||||
@ -259,10 +267,8 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
},
|
||||
},
|
||||
RTCConstraints: {
|
||||
mandatory: {
|
||||
OfferToReceiveAudio: true,
|
||||
OfferToReceiveVideo: false,
|
||||
},
|
||||
offerToReceiveAudio: true,
|
||||
offerToReceiveVideo: false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -273,7 +279,14 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
return new Promise((resolve) => {
|
||||
const { mediaHandler } = currentSession;
|
||||
|
||||
const connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected'];
|
||||
let connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected'];
|
||||
// Edge sends a connected first and then a completed, but the call isn't ready until
|
||||
// the completed comes in. Due to the way that we have the listeners set up, the only
|
||||
// way to ignore one status is to not listen for it.
|
||||
if (browser().name === 'edge') {
|
||||
connectionCompletedEvents = ['iceConnectionCompleted'];
|
||||
}
|
||||
|
||||
const handleConnectionCompleted = () => {
|
||||
connectionCompletedEvents.forEach(e => mediaHandler.off(e, handleConnectionCompleted));
|
||||
// We have to delay notifying that the call is connected because it is sometimes not
|
||||
@ -295,9 +308,9 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
});
|
||||
}
|
||||
|
||||
const mappedCause = cause in this.errorCodes ?
|
||||
this.errorCodes[cause] :
|
||||
this.baseErrorCodes.GENERIC_ERROR;
|
||||
const mappedCause = cause in this.errorCodes
|
||||
? this.errorCodes[cause]
|
||||
: this.baseErrorCodes.GENERIC_ERROR;
|
||||
|
||||
return this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
|
@ -3,7 +3,7 @@ import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Breakouts from '/imports/api/breakouts';
|
||||
|
||||
export default function requestJoinURL(credentials, { breakoutId }) {
|
||||
export default function requestJoinURL(credentials, { breakoutId, userId: userIdToInvite }) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
|
||||
@ -12,16 +12,19 @@ export default function requestJoinURL(credentials, { breakoutId }) {
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(requesterToken, String);
|
||||
const userId = userIdToInvite || requesterUserId;
|
||||
const Breakout = Breakouts.findOne({ breakoutId });
|
||||
const BreakoutUser = Breakout.users.filter(user => user.userId === requesterUserId).shift();
|
||||
if (BreakoutUser) return BreakoutUser.redirectToHtml5JoinURL;
|
||||
const BreakoutUser = Breakout.users.filter(user => user.userId === userId).shift();
|
||||
|
||||
if (BreakoutUser) return null;
|
||||
const eventName = 'RequestBreakoutJoinURLReqMsg';
|
||||
|
||||
return RedisPubSub.publishUserMessage(
|
||||
CHANNEL, eventName, meetingId, requesterUserId,
|
||||
{
|
||||
meetingId,
|
||||
breakoutId,
|
||||
userId: requesterUserId,
|
||||
userId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
5
bigbluebutton-html5/imports/api/external-videos/index.js
Normal file
5
bigbluebutton-html5/imports/api/external-videos/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const Streamer = new Meteor.Streamer('videos');
|
||||
|
||||
export default Streamer;
|
@ -0,0 +1,18 @@
|
||||
import ExternalVideoStreamer from '/imports/api/external-videos';
|
||||
import Users from '/imports/api/users';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import './methods';
|
||||
|
||||
ExternalVideoStreamer.allowRead('all');
|
||||
ExternalVideoStreamer.allowWrite('all');
|
||||
|
||||
const allowFromPresenter = (eventName, { userId }) => {
|
||||
const user = Users.findOne({ userId });
|
||||
const ret = user && user.presenter;
|
||||
|
||||
Logger.debug('ExternalVideo Streamer auth userid:', userId, ' event: ', eventName, ' suc: ', ret);
|
||||
|
||||
return ret || eventName === 'viewerJoined';
|
||||
};
|
||||
|
||||
ExternalVideoStreamer.allowEmit(allowFromPresenter);
|
@ -0,0 +1,8 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import startWatchingExternalVideo from './methods/startWatchingExternalVideo';
|
||||
import stopWatchingExternalVideo from './methods/stopWatchingExternalVideo';
|
||||
|
||||
Meteor.methods({
|
||||
startWatchingExternalVideo,
|
||||
stopWatchingExternalVideo,
|
||||
});
|
@ -0,0 +1,26 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
|
||||
export default function startStream(credentials, options) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'StartExternalVideoMsg';
|
||||
|
||||
const { meetingId, requesterUserId } = credentials;
|
||||
const { externalVideoUrl } = options;
|
||||
|
||||
Logger.info(' user sharing a new youtube video: ', credentials);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(externalVideoUrl, String);
|
||||
|
||||
Meetings.update({ meetingId }, { $set: { externalVideoUrl } });
|
||||
|
||||
const payload = { externalVideoUrl };
|
||||
|
||||
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
|
||||
export default function startStream(credentials) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'StopExternalVideoMsg';
|
||||
|
||||
const { meetingId, requesterUserId } = credentials;
|
||||
|
||||
Logger.info(' user sharing a new youtube video: ', credentials);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
Meetings.update({ meetingId }, { $set: { externalVideoUrl: null } });
|
||||
const payload = {};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
}
|
@ -11,20 +11,19 @@ export default function clearGroupChatMsg(meetingId, chatId) {
|
||||
if (chatId) {
|
||||
GroupChatMsg.remove({ meetingId, chatId }, () => {
|
||||
Logger.info(`Cleared GroupChatMsg (${meetingId}, ${chatId})`);
|
||||
const clearMsg = {
|
||||
color: '0',
|
||||
timestamp: Date.now(),
|
||||
correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`,
|
||||
sender: {
|
||||
id: PUBLIC_CHAT_SYSTEM_ID,
|
||||
name: '',
|
||||
},
|
||||
message: CHAT_CLEAR_MESSAGE,
|
||||
};
|
||||
|
||||
return addGroupChatMsg(meetingId, PUBLIC_GROUP_CHAT_ID, clearMsg);
|
||||
});
|
||||
|
||||
const clearMsg = {
|
||||
color: '0',
|
||||
timestamp: Date.now(),
|
||||
correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`,
|
||||
sender: {
|
||||
id: PUBLIC_CHAT_SYSTEM_ID,
|
||||
name: '',
|
||||
},
|
||||
message: CHAT_CLEAR_MESSAGE,
|
||||
};
|
||||
|
||||
return addGroupChatMsg(meetingId, PUBLIC_GROUP_CHAT_ID, clearMsg);
|
||||
}
|
||||
|
||||
if (meetingId) {
|
||||
|
@ -6,6 +6,7 @@ import handleMeetingDestruction from './handlers/meetingDestruction';
|
||||
import handleMeetingLocksChange from './handlers/meetingLockChange';
|
||||
import handleUserLockChange from './handlers/userLockChange';
|
||||
import handleRecordingStatusChange from './handlers/recordingStatusChange';
|
||||
import handleRecordingTimerChange from './handlers/recordingTimerChange';
|
||||
import handleChangeWebcamOnlyModerator from './handlers/webcamOnlyModerator';
|
||||
|
||||
RedisPubSub.on('MeetingCreatedEvtMsg', handleMeetingCreation);
|
||||
@ -15,4 +16,5 @@ RedisPubSub.on('MeetingDestroyedEvtMsg', handleMeetingDestruction);
|
||||
RedisPubSub.on('LockSettingsInMeetingChangedEvtMsg', handleMeetingLocksChange);
|
||||
RedisPubSub.on('UserLockedInMeetingEvtMsg', handleUserLockChange);
|
||||
RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange);
|
||||
RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange);
|
||||
RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator);
|
||||
|
@ -0,0 +1,29 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function handleRecordingStatusChange({ body }, meetingId) {
|
||||
const { time } = body;
|
||||
|
||||
check(meetingId, String);
|
||||
|
||||
check(body, {
|
||||
time: Number,
|
||||
});
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: { 'recordProp.time': time },
|
||||
};
|
||||
|
||||
const cb = (err) => {
|
||||
if (err) {
|
||||
Logger.error(`Changing recording time: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
return Meetings.upsert(selector, modifier, cb);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import flat from 'flat';
|
||||
import { check } from 'meteor/check';
|
||||
import { check, Match } from 'meteor/check';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
@ -42,11 +42,11 @@ export default function addMeeting(meeting) {
|
||||
modOnlyMessage: String,
|
||||
welcomeMsgTemplate: String,
|
||||
},
|
||||
recordProp: {
|
||||
recordProp: Match.ObjectIncluding({
|
||||
allowStartStopRecording: Boolean,
|
||||
autoStartRecording: Boolean,
|
||||
record: Boolean,
|
||||
},
|
||||
record: Boolean
|
||||
}),
|
||||
password: {
|
||||
viewerPass: String,
|
||||
moderatorPass: String,
|
||||
|
@ -17,7 +17,7 @@ export default function addUserSettings(credentials, meetingId, userId, settings
|
||||
'forceListenOnly',
|
||||
'skipCheck',
|
||||
'clientTitle',
|
||||
'lockOnJoin', // NOT IMPLEMENTED YET
|
||||
'lockOnJoin',
|
||||
'askForFeedbackOnLogout',
|
||||
// BRANDING
|
||||
'displayBrandingArea',
|
||||
|
@ -20,4 +20,3 @@ RedisPubSub.on('GuestsWaitingForApprovalEvtMsg', handleGuestsWaitingForApproval)
|
||||
RedisPubSub.on('GuestsWaitingApprovedEvtMsg', handleGuestApproved);
|
||||
RedisPubSub.on('UserEjectedFromMeetingEvtMsg', handleUserEjected);
|
||||
RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole);
|
||||
|
||||
|
@ -40,6 +40,7 @@ export default function handleValidateAuthToken({ body }, meetingId) {
|
||||
$set: {
|
||||
validated: valid,
|
||||
approved: !waitForApproval,
|
||||
loginTime: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -4,6 +4,7 @@ import setEmojiStatus from './methods/setEmojiStatus';
|
||||
import assignPresenter from './methods/assignPresenter';
|
||||
import changeRole from './methods/changeRole';
|
||||
import removeUser from './methods/removeUser';
|
||||
import toggleUserLock from './methods/toggleUserLock';
|
||||
|
||||
Meteor.methods({
|
||||
setEmojiStatus,
|
||||
@ -11,4 +12,5 @@ Meteor.methods({
|
||||
changeRole,
|
||||
removeUser,
|
||||
validateAuthToken,
|
||||
toggleUserLock,
|
||||
});
|
||||
|
@ -0,0 +1,29 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function toggleUserLock(credentials, userId, lock) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'LockUserInMeetingCmdMsg';
|
||||
|
||||
const { meetingId, requesterUserId: lockedBy } = credentials;
|
||||
|
||||
check(meetingId, String);
|
||||
check(lockedBy, String);
|
||||
check(userId, String);
|
||||
check(lock, Boolean);
|
||||
|
||||
const payload = {
|
||||
lockedBy,
|
||||
userId,
|
||||
lock,
|
||||
};
|
||||
|
||||
Logger.verbose(`User ${lockedBy} updated lock status from ${userId} to ${lock}
|
||||
in meeting ${meetingId}`);
|
||||
|
||||
|
||||
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, lockedBy, payload);
|
||||
}
|
@ -78,7 +78,6 @@ export default function addUser(meetingId, user) {
|
||||
roles: [ROLE_VIEWER.toLowerCase()],
|
||||
sortName: user.name.trim().toLowerCase(),
|
||||
color,
|
||||
logTime: Date.now(),
|
||||
},
|
||||
flat(user),
|
||||
),
|
||||
|
@ -45,11 +45,16 @@ class Base extends Component {
|
||||
this.updateLoadingState = this.updateLoadingState.bind(this);
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
const { approved } = this.props;
|
||||
componentDidUpdate(prevProps) {
|
||||
const { ejected, approved } = this.props;
|
||||
const { loading } = this.state;
|
||||
|
||||
if (approved && loading) this.updateLoadingState(false);
|
||||
|
||||
if (prevProps.ejected || ejected) {
|
||||
Session.set('codeError', '403');
|
||||
Session.set('isMeetingEnded', true);
|
||||
}
|
||||
}
|
||||
|
||||
updateLoadingState(loading = false) {
|
||||
@ -173,6 +178,7 @@ const BaseContainer = withTracker(() => {
|
||||
const subscriptionsReady = subscriptionsHandlers.every(handler => handler.ready());
|
||||
return {
|
||||
approved: Users.findOne({ userId: Auth.userID, approved: true, guest: true }),
|
||||
ejected: Users.findOne({ userId: Auth.userID, ejected: true }),
|
||||
meetingEnded: Session.get('isMeetingEnded'),
|
||||
locale,
|
||||
subscriptionsReady,
|
||||
|
@ -1,9 +1,49 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import LoadingScreen from '/imports/ui/components/loading-screen/component';
|
||||
|
||||
// currently supported locales.
|
||||
import bg from 'react-intl/locale-data/bg';
|
||||
import de from 'react-intl/locale-data/de';
|
||||
import el from 'react-intl/locale-data/el';
|
||||
import en from 'react-intl/locale-data/en';
|
||||
import es from 'react-intl/locale-data/es';
|
||||
import fa from 'react-intl/locale-data/fa';
|
||||
import fr from 'react-intl/locale-data/fr';
|
||||
import id from 'react-intl/locale-data/id';
|
||||
import it from 'react-intl/locale-data/it';
|
||||
import ja from 'react-intl/locale-data/ja';
|
||||
import km from 'react-intl/locale-data/km';
|
||||
import pl from 'react-intl/locale-data/pl';
|
||||
import pt from 'react-intl/locale-data/pt';
|
||||
import ru from 'react-intl/locale-data/ru';
|
||||
import tr from 'react-intl/locale-data/tr';
|
||||
import uk from 'react-intl/locale-data/uk';
|
||||
import zh from 'react-intl/locale-data/zh';
|
||||
|
||||
|
||||
addLocaleData([
|
||||
...bg,
|
||||
...de,
|
||||
...el,
|
||||
...en,
|
||||
...es,
|
||||
...fa,
|
||||
...fr,
|
||||
...id,
|
||||
...it,
|
||||
...ja,
|
||||
...km,
|
||||
...pl,
|
||||
...pt,
|
||||
...ru,
|
||||
...tr,
|
||||
...uk,
|
||||
...zh,
|
||||
]);
|
||||
|
||||
const propTypes = {
|
||||
locale: PropTypes.string,
|
||||
children: PropTypes.element.isRequired,
|
||||
@ -29,14 +69,18 @@ class IntlStartup extends Component {
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.fetchLocalizedMessages(this.props.locale);
|
||||
const { locale } = this.props;
|
||||
this.fetchLocalizedMessages(locale);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
if (!this.state.fetching
|
||||
&& this.state.normalizedLocale
|
||||
&& nextProps.locale.toLowerCase() !== this.state.normalizedLocale.toLowerCase()) {
|
||||
this.fetchLocalizedMessages(nextProps.locale);
|
||||
const { fetching, normalizedLocale } = this.state;
|
||||
const { locale } = nextProps;
|
||||
|
||||
if (!fetching
|
||||
&& normalizedLocale
|
||||
&& locale.toLowerCase() !== normalizedLocale.toLowerCase()) {
|
||||
this.fetchLocalizedMessages(locale);
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,9 +113,12 @@ class IntlStartup extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.fetching ? <LoadingScreen /> : (
|
||||
<IntlProvider locale={DEFAULT_LANGUAGE} messages={this.state.messages}>
|
||||
{this.props.children}
|
||||
const { fetching, normalizedLocale, messages } = this.state;
|
||||
const { children } = this.props;
|
||||
|
||||
return fetching ? <LoadingScreen /> : (
|
||||
<IntlProvider locale={normalizedLocale} messages={messages}>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
|
||||
messages = Object.assign(messages, JSON.parse(data));
|
||||
normalizedLocale = locale;
|
||||
} catch (e) {
|
||||
Logger.error(`'Could not process locale ${locale}:${e}`);
|
||||
// Getting here means the locale is not available on the files.
|
||||
}
|
||||
});
|
||||
@ -71,6 +72,7 @@ WebApp.connectHandlers.use('/locales', (req, res) => {
|
||||
name: Langmap[locale].nativeName,
|
||||
}));
|
||||
} catch (e) {
|
||||
Logger.error(`'Could not process locales error: ${e}`);
|
||||
// Getting here means the locale is not available on the files.
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ const makeEnvelope = (channel, eventName, header, body) => {
|
||||
|
||||
const makeDebugger = enabled => (message) => {
|
||||
if (!enabled) return;
|
||||
Logger.info(`REDIS: ${message}`);
|
||||
Logger.debug(`REDIS: ${message}`);
|
||||
};
|
||||
|
||||
class MettingMessageQueue {
|
||||
@ -166,9 +166,6 @@ class RedisPubSub {
|
||||
return;
|
||||
}
|
||||
|
||||
// Please keep this log until the message handling is solid
|
||||
console.warn(` ~~~~ REDIS RECEIVED: ${eventName} ${message}`);
|
||||
|
||||
const queueId = meetingId || NO_MEETING_ID;
|
||||
|
||||
if (!(queueId in this.mettingsQueues)) {
|
||||
@ -232,9 +229,6 @@ class RedisPubSub {
|
||||
|
||||
const envelope = makeEnvelope(channel, eventName, header, payload);
|
||||
|
||||
// Please keep this log until the message handling is solid
|
||||
console.warn(` ~~~~ REDIS PUBLISHING: ${envelope}`);
|
||||
|
||||
return this.pub.publish(channel, envelope, RedisPubSub.handlePublishError);
|
||||
}
|
||||
}
|
||||
|
@ -10,27 +10,24 @@ import DropdownList from '/imports/ui/components/dropdown/list/component';
|
||||
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
|
||||
import PresentationUploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||
import BreakoutRoom from '../create-breakout-room/component';
|
||||
import { styles } from '../styles';
|
||||
import ActionBarService from '../service';
|
||||
|
||||
import ExternalVideoModal from '/imports/ui/components/external-video-player/modal/container';
|
||||
|
||||
const propTypes = {
|
||||
isUserPresenter: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
isUserModerator: PropTypes.bool.isRequired,
|
||||
allowStartStopRecording: PropTypes.bool.isRequired,
|
||||
isRecording: PropTypes.bool.isRequired,
|
||||
record: PropTypes.bool.isRequired,
|
||||
toggleRecording: PropTypes.func.isRequired,
|
||||
meetingIsBreakout: PropTypes.bool.isRequired,
|
||||
hasBreakoutRoom: PropTypes.bool.isRequired,
|
||||
createBreakoutRoom: PropTypes.func.isRequired,
|
||||
meetingName: PropTypes.string.isRequired,
|
||||
shortcuts: PropTypes.string.isRequired,
|
||||
users: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
handleTakePresenter: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -62,14 +59,6 @@ const intlMessages = defineMessages({
|
||||
id: 'app.actionsBar.actionsDropdown.stopDesktopShareDesc',
|
||||
description: 'adds context to stop desktop share option',
|
||||
},
|
||||
startRecording: {
|
||||
id: 'app.actionsBar.actionsDropdown.startRecording',
|
||||
description: 'start recording option',
|
||||
},
|
||||
stopRecording: {
|
||||
id: 'app.actionsBar.actionsDropdown.stopRecording',
|
||||
description: 'stop recording option',
|
||||
},
|
||||
pollBtnLabel: {
|
||||
id: 'app.actionsBar.actionsDropdown.pollBtnLabel',
|
||||
description: 'poll menu toggle button label',
|
||||
@ -86,6 +75,22 @@ const intlMessages = defineMessages({
|
||||
id: 'app.actionsBar.actionsDropdown.createBreakoutRoomDesc',
|
||||
description: 'Description of create breakout room option',
|
||||
},
|
||||
invitationItem: {
|
||||
id: 'app.invitation.title',
|
||||
description: 'invitation to breakout title',
|
||||
},
|
||||
takePresenter: {
|
||||
id: 'app.actionsBar.actionsDropdown.takePresenter',
|
||||
description: 'Label for take presenter role option',
|
||||
},
|
||||
takePresenterDesc: {
|
||||
id: 'app.actionsBar.actionsDropdown.takePresenterDesc',
|
||||
description: 'Description of take presenter role option',
|
||||
},
|
||||
externalVideoLabel: {
|
||||
id: 'app.actionsBar.actionsDropdown.shareExternalVideo',
|
||||
description: 'Share external video button',
|
||||
},
|
||||
});
|
||||
|
||||
class ActionsDropdown extends Component {
|
||||
@ -93,21 +98,17 @@ class ActionsDropdown extends Component {
|
||||
super(props);
|
||||
this.handlePresentationClick = this.handlePresentationClick.bind(this);
|
||||
this.handleCreateBreakoutRoomClick = this.handleCreateBreakoutRoomClick.bind(this);
|
||||
}
|
||||
this.onCreateBreakouts = this.onCreateBreakouts.bind(this);
|
||||
this.onInvitationUsers = this.onInvitationUsers.bind(this);
|
||||
|
||||
componentWillMount() {
|
||||
this.presentationItemId = _.uniqueId('action-item-');
|
||||
this.recordId = _.uniqueId('action-item-');
|
||||
this.pollId = _.uniqueId('action-item-');
|
||||
this.createBreakoutRoomId = _.uniqueId('action-item-');
|
||||
}
|
||||
this.takePresenterId = _.uniqueId('action-item-');
|
||||
|
||||
componentDidMount() {
|
||||
if (Meteor.settings.public.allowOutsideCommands.toggleRecording
|
||||
|| getFromUserSettings('outsideToggleRecording', false)) {
|
||||
ActionBarService.connectRecordingObserver();
|
||||
window.addEventListener('message', ActionBarService.processOutsideToggleRecording);
|
||||
}
|
||||
this.handlePresentationClick = this.handlePresentationClick.bind(this);
|
||||
this.handleCreateBreakoutRoomClick = this.handleCreateBreakoutRoomClick.bind(this);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
@ -118,17 +119,25 @@ class ActionsDropdown extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onCreateBreakouts() {
|
||||
return this.handleCreateBreakoutRoomClick(false);
|
||||
}
|
||||
|
||||
onInvitationUsers() {
|
||||
return this.handleCreateBreakoutRoomClick(true);
|
||||
}
|
||||
|
||||
getAvailableActions() {
|
||||
const {
|
||||
intl,
|
||||
isUserPresenter,
|
||||
isUserModerator,
|
||||
allowStartStopRecording,
|
||||
isRecording,
|
||||
record,
|
||||
toggleRecording,
|
||||
allowExternalVideo,
|
||||
meetingIsBreakout,
|
||||
hasBreakoutRoom,
|
||||
getUsersNotAssigned,
|
||||
users,
|
||||
handleTakePresenter,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -136,16 +145,26 @@ class ActionsDropdown extends Component {
|
||||
pollBtnDesc,
|
||||
presentationLabel,
|
||||
presentationDesc,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
createBreakoutRoom,
|
||||
createBreakoutRoomDesc,
|
||||
invitationItem,
|
||||
takePresenter,
|
||||
takePresenterDesc,
|
||||
} = intlMessages;
|
||||
|
||||
const {
|
||||
formatMessage,
|
||||
} = intl;
|
||||
|
||||
const canCreateBreakout = isUserModerator
|
||||
&& !meetingIsBreakout
|
||||
&& !hasBreakoutRoom;
|
||||
|
||||
const canInviteUsers = isUserModerator
|
||||
&& !meetingIsBreakout
|
||||
&& hasBreakoutRoom
|
||||
&& getUsersNotAssigned(users).length;
|
||||
|
||||
return _.compact([
|
||||
(isUserPresenter
|
||||
? (
|
||||
@ -160,7 +179,15 @@ class ActionsDropdown extends Component {
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: null),
|
||||
: (
|
||||
<DropdownListItem
|
||||
icon="presentation"
|
||||
label={formatMessage(takePresenter)}
|
||||
description={formatMessage(takePresenterDesc)}
|
||||
key={this.takePresenterId}
|
||||
onClick={() => handleTakePresenter()}
|
||||
/>
|
||||
)),
|
||||
(isUserPresenter
|
||||
? (
|
||||
<DropdownListItem
|
||||
@ -173,51 +200,72 @@ class ActionsDropdown extends Component {
|
||||
/>
|
||||
)
|
||||
: null),
|
||||
(record && isUserModerator && allowStartStopRecording
|
||||
(isUserPresenter && allowExternalVideo
|
||||
? (
|
||||
<DropdownListItem
|
||||
icon="record"
|
||||
label={formatMessage(isRecording
|
||||
? stopRecording : startRecording)}
|
||||
description={formatMessage(isRecording
|
||||
? stopRecording : startRecording)}
|
||||
key={this.recordId}
|
||||
onClick={toggleRecording}
|
||||
icon="video"
|
||||
label={intl.formatMessage(intlMessages.externalVideoLabel)}
|
||||
description="External Video"
|
||||
key="external-video"
|
||||
onClick={this.handleExternalVideoClick}
|
||||
/>
|
||||
)
|
||||
: null),
|
||||
(isUserModerator && !meetingIsBreakout && !hasBreakoutRoom
|
||||
(canCreateBreakout
|
||||
? (
|
||||
<DropdownListItem
|
||||
icon="rooms"
|
||||
label={formatMessage(createBreakoutRoom)}
|
||||
description={formatMessage(createBreakoutRoomDesc)}
|
||||
key={this.createBreakoutRoomId}
|
||||
onClick={this.handleCreateBreakoutRoomClick}
|
||||
onClick={this.onCreateBreakouts}
|
||||
/>
|
||||
)
|
||||
: null),
|
||||
(canInviteUsers
|
||||
? (
|
||||
<DropdownListItem
|
||||
icon="rooms"
|
||||
label={formatMessage(invitationItem)}
|
||||
key={this.createBreakoutRoomId}
|
||||
onClick={this.onInvitationUsers}
|
||||
/>
|
||||
)
|
||||
: null),
|
||||
]);
|
||||
}
|
||||
|
||||
handleExternalVideoClick = () => {
|
||||
this.props.mountModal(<ExternalVideoModal />);
|
||||
}
|
||||
|
||||
handlePresentationClick() {
|
||||
const { mountModal } = this.props;
|
||||
mountModal(<PresentationUploaderContainer />);
|
||||
}
|
||||
|
||||
handleCreateBreakoutRoomClick() {
|
||||
handleCreateBreakoutRoomClick(isInvitation) {
|
||||
const {
|
||||
createBreakoutRoom,
|
||||
mountModal,
|
||||
meetingName,
|
||||
users,
|
||||
getUsersNotAssigned,
|
||||
getBreakouts,
|
||||
sendInvitation,
|
||||
} = this.props;
|
||||
|
||||
mountModal(
|
||||
<BreakoutRoom
|
||||
createBreakoutRoom={createBreakoutRoom}
|
||||
meetingName={meetingName}
|
||||
users={users}
|
||||
{...{
|
||||
createBreakoutRoom,
|
||||
meetingName,
|
||||
users,
|
||||
getUsersNotAssigned,
|
||||
isInvitation,
|
||||
getBreakouts,
|
||||
sendInvitation,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ import { styles } from './styles.scss';
|
||||
import DesktopShare from './desktop-share/component';
|
||||
import ActionsDropdown from './actions-dropdown/component';
|
||||
import AudioControlsContainer from '../audio/audio-controls/container';
|
||||
import JoinVideoOptionsContainer from '../video-provider/video-menu/container';
|
||||
import JoinVideoOptionsContainer from '../video-provider/video-button/container';
|
||||
|
||||
import PresentationOptionsContainer from './presentation-options/component';
|
||||
|
||||
class ActionsBar extends React.PureComponent {
|
||||
render() {
|
||||
@ -25,6 +27,12 @@ class ActionsBar extends React.PureComponent {
|
||||
hasBreakoutRoom,
|
||||
meetingName,
|
||||
users,
|
||||
isLayoutSwapped,
|
||||
toggleSwapLayout,
|
||||
getUsersNotAssigned,
|
||||
sendInvitation,
|
||||
getBreakouts,
|
||||
handleTakePresenter,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -34,6 +42,8 @@ class ActionsBar extends React.PureComponent {
|
||||
} = recordSettingsList;
|
||||
|
||||
const actionBarClasses = {};
|
||||
const { enableExternalVideo } = Meteor.settings.public.app;
|
||||
|
||||
actionBarClasses[styles.centerWithActions] = isUserPresenter;
|
||||
actionBarClasses[styles.center] = true;
|
||||
|
||||
@ -44,6 +54,7 @@ class ActionsBar extends React.PureComponent {
|
||||
isUserPresenter,
|
||||
isUserModerator,
|
||||
allowStartStopRecording,
|
||||
allowExternalVideo: enableExternalVideo,
|
||||
isRecording,
|
||||
record,
|
||||
toggleRecording,
|
||||
@ -52,6 +63,10 @@ class ActionsBar extends React.PureComponent {
|
||||
hasBreakoutRoom,
|
||||
meetingName,
|
||||
users,
|
||||
getUsersNotAssigned,
|
||||
sendInvitation,
|
||||
getBreakouts,
|
||||
handleTakePresenter,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -78,6 +93,16 @@ class ActionsBar extends React.PureComponent {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
{ isLayoutSwapped
|
||||
? (
|
||||
<PresentationOptionsContainer
|
||||
toggleSwapLayout={toggleSwapLayout}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -3,11 +3,14 @@ import { withTracker } from 'meteor/react-meteor-data';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import ActionsBar from './component';
|
||||
import Service from './service';
|
||||
import VideoService from '../video-provider/service';
|
||||
import { shareScreen, unshareScreen, isVideoBroadcasting } from '../screenshare/service';
|
||||
|
||||
import MediaService, { getSwapLayout } from '../media/service';
|
||||
|
||||
const ActionsBarContainer = props => <ActionsBar {...props} />;
|
||||
|
||||
export default withTracker(() => {
|
||||
@ -23,6 +26,7 @@ export default withTracker(() => {
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
isUserPresenter: Service.isUserPresenter(),
|
||||
isUserModerator: Service.isUserModerator(),
|
||||
@ -40,5 +44,11 @@ export default withTracker(() => {
|
||||
hasBreakoutRoom: Service.hasBreakoutRoom(),
|
||||
meetingName: Service.meetingName(),
|
||||
users: Service.users(),
|
||||
isLayoutSwapped: getSwapLayout(),
|
||||
toggleSwapLayout: MediaService.toggleSwapLayout,
|
||||
sendInvitation: Service.sendInvitation,
|
||||
getBreakouts: Service.getBreakouts,
|
||||
getUsersNotAssigned: Service.getUsersNotAssigned,
|
||||
handleTakePresenter: Service.takePresenterRole,
|
||||
};
|
||||
})(ActionsBarContainer);
|
||||
|
@ -77,6 +77,14 @@ const intlMessages = defineMessages({
|
||||
id: 'app.audio.backLabel',
|
||||
description: 'Back label',
|
||||
},
|
||||
invitationTitle: {
|
||||
id: 'app.invitation.title',
|
||||
description: 'isInvitationto breakout title',
|
||||
},
|
||||
invitationConfirm: {
|
||||
id: 'app.invitation.confirm',
|
||||
description: 'Invitation to breakout confirm button label',
|
||||
},
|
||||
});
|
||||
const MIN_BREAKOUT_ROOMS = 2;
|
||||
const MAX_BREAKOUT_ROOMS = 8;
|
||||
@ -94,6 +102,7 @@ class BreakoutRoom extends Component {
|
||||
this.setFreeJoin = this.setFreeJoin.bind(this);
|
||||
this.getUserByRoom = this.getUserByRoom.bind(this);
|
||||
this.onAssignRandomly = this.onAssignRandomly.bind(this);
|
||||
this.onInviteBreakout = this.onInviteBreakout.bind(this);
|
||||
this.renderUserItemByRoom = this.renderUserItemByRoom.bind(this);
|
||||
this.renderRoomsGrid = this.renderRoomsGrid.bind(this);
|
||||
this.renderBreakoutForm = this.renderBreakoutForm.bind(this);
|
||||
@ -103,7 +112,9 @@ class BreakoutRoom extends Component {
|
||||
this.renderMobile = this.renderMobile.bind(this);
|
||||
this.renderButtonSetLevel = this.renderButtonSetLevel.bind(this);
|
||||
this.renderSelectUserScreen = this.renderSelectUserScreen.bind(this);
|
||||
this.renderTitle = this.renderTitle.bind(this);
|
||||
this.handleDismiss = this.handleDismiss.bind(this);
|
||||
this.setInvitationConfig = this.setInvitationConfig.bind(this);
|
||||
|
||||
this.state = {
|
||||
numberOfRooms: MIN_BREAKOUT_ROOMS,
|
||||
@ -116,10 +127,18 @@ class BreakoutRoom extends Component {
|
||||
preventClosing: true,
|
||||
valid: true,
|
||||
};
|
||||
|
||||
this.breakoutFormId = _.uniqueId('breakout-form-');
|
||||
this.freeJoinId = _.uniqueId('free-join-check-');
|
||||
this.btnLevelId = _.uniqueId('btn-set-level-');
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { isInvitation } = this.props;
|
||||
this.setRoomUsers();
|
||||
if (isInvitation) {
|
||||
this.setInvitationConfig();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevstate) {
|
||||
@ -140,7 +159,7 @@ class BreakoutRoom extends Component {
|
||||
freeJoin,
|
||||
} = this.state;
|
||||
|
||||
if (users.length === this.getUserByRoom(0).length) {
|
||||
if (users.length === this.getUserByRoom(0).length && !freeJoin) {
|
||||
this.setState({ valid: false });
|
||||
return;
|
||||
}
|
||||
@ -160,17 +179,46 @@ class BreakoutRoom extends Component {
|
||||
Session.set('isUserListOpen', true);
|
||||
}
|
||||
|
||||
onInviteBreakout() {
|
||||
const { getBreakouts, sendInvitation } = this.props;
|
||||
const { users } = this.state;
|
||||
const breakouts = getBreakouts();
|
||||
if (users.length === this.getUserByRoom(0).length) {
|
||||
this.setState({ valid: false });
|
||||
return;
|
||||
}
|
||||
|
||||
breakouts.forEach((breakout) => {
|
||||
const { breakoutId } = breakout;
|
||||
const breakoutUsers = this.getUserByRoom(breakout.sequence);
|
||||
breakoutUsers.forEach(user => sendInvitation(breakoutId, user.userId));
|
||||
});
|
||||
|
||||
this.setState({ preventClosing: false });
|
||||
}
|
||||
|
||||
onAssignRandomly() {
|
||||
const { numberOfRooms } = this.state;
|
||||
return this.getUserByRoom(0)
|
||||
.filter(user => !user.isModerator)
|
||||
.forEach(user => this.changeUserRoom(user.userId, Math.floor(Math.random() * (numberOfRooms) + 1)));
|
||||
}
|
||||
|
||||
setInvitationConfig() {
|
||||
const { getBreakouts } = this.props;
|
||||
this.setState({
|
||||
numberOfRooms: getBreakouts().length,
|
||||
formFillLevel: 2,
|
||||
});
|
||||
}
|
||||
|
||||
setRoomUsers() {
|
||||
const { users } = this.props;
|
||||
const roomUsers = users.map(user => ({
|
||||
const { users, getUsersNotAssigned } = this.props;
|
||||
|
||||
const roomUsers = getUsersNotAssigned(users).map(user => ({
|
||||
userId: user.userId,
|
||||
userName: user.name,
|
||||
isModerator: user.moderator,
|
||||
room: 0,
|
||||
}));
|
||||
|
||||
@ -208,7 +256,7 @@ class BreakoutRoom extends Component {
|
||||
|
||||
changeUserRoom(userId, room) {
|
||||
const { users } = this.state;
|
||||
|
||||
|
||||
const idxUser = users.findIndex(user => user.userId === userId);
|
||||
users[idxUser].room = room;
|
||||
this.setState({ users });
|
||||
@ -251,7 +299,7 @@ class BreakoutRoom extends Component {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.boxContainer}>
|
||||
<div className={styles.boxContainer} key="rooms-grid-">
|
||||
<label htmlFor="BreakoutRoom" className={!valid ? styles.changeToWarn : null}>
|
||||
<p
|
||||
className={styles.freeJoinLabel}
|
||||
@ -277,7 +325,7 @@ class BreakoutRoom extends Component {
|
||||
<div className={styles.breakoutBox} onDrop={drop(value)} onDragOver={allowDrop}>
|
||||
{this.renderUserItemByRoom(value)}
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
@ -285,13 +333,18 @@ class BreakoutRoom extends Component {
|
||||
}
|
||||
|
||||
renderBreakoutForm() {
|
||||
const { intl } = this.props;
|
||||
const {
|
||||
intl,
|
||||
isInvitation,
|
||||
} = this.props;
|
||||
const {
|
||||
numberOfRooms,
|
||||
durationTime,
|
||||
} = this.state;
|
||||
if (isInvitation) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.breakoutSettings}>
|
||||
<div className={styles.breakoutSettings} key={this.breakoutFormId}>
|
||||
<label htmlFor="numberOfRooms">
|
||||
<p className={styles.labelText}>{intl.formatMessage(intlMessages.numberOfRooms)}</p>
|
||||
<select
|
||||
@ -341,7 +394,13 @@ class BreakoutRoom extends Component {
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
<span className={styles.randomText} role="button" onClick={this.onAssignRandomly}>{intl.formatMessage(intlMessages.randomlyAssign)}</span>
|
||||
<span
|
||||
className={styles.randomText}
|
||||
role="button"
|
||||
onClick={this.onAssignRandomly}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.randomlyAssign)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -363,10 +422,11 @@ class BreakoutRoom extends Component {
|
||||
}
|
||||
|
||||
renderFreeJoinCheck() {
|
||||
const { intl } = this.props;
|
||||
const { intl, isInvitation } = this.props;
|
||||
if (isInvitation) return null;
|
||||
const { freeJoin } = this.state;
|
||||
return (
|
||||
<label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel}>
|
||||
<label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel} key={this.freeJoinId}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.freeJoinCheckbox}
|
||||
@ -416,7 +476,7 @@ class BreakoutRoom extends Component {
|
||||
}
|
||||
|
||||
renderRoomSortList() {
|
||||
const { intl } = this.props;
|
||||
const { intl, isInvitation } = this.props;
|
||||
const { numberOfRooms } = this.state;
|
||||
const onClick = roomNumber => this.setState({ formFillLevel: 3, roomSelected: roomNumber });
|
||||
return (
|
||||
@ -440,7 +500,7 @@ class BreakoutRoom extends Component {
|
||||
))
|
||||
}
|
||||
</span>
|
||||
{this.renderButtonSetLevel(1, intl.formatMessage(intlMessages.backLabel))}
|
||||
{ isInvitation || this.renderButtonSetLevel(1, intl.formatMessage(intlMessages.backLabel))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -478,12 +538,22 @@ class BreakoutRoom extends Component {
|
||||
size="lg"
|
||||
label={label}
|
||||
onClick={() => this.setState({ formFillLevel: level })}
|
||||
key={this.btnLevelId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
renderTitle() {
|
||||
const { intl } = this.props;
|
||||
return (
|
||||
<p className={styles.subTitle}>
|
||||
{intl.formatMessage(intlMessages.breakoutRoomDesc)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, isInvitation } = this.props;
|
||||
const { preventClosing } = this.state;
|
||||
|
||||
const BROWSER_RESULTS = browser();
|
||||
@ -491,11 +561,17 @@ class BreakoutRoom extends Component {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={intl.formatMessage(intlMessages.breakoutRoomTitle)}
|
||||
title={
|
||||
isInvitation
|
||||
? intl.formatMessage(intlMessages.invitationTitle)
|
||||
: intl.formatMessage(intlMessages.breakoutRoomTitle)
|
||||
}
|
||||
confirm={
|
||||
{
|
||||
label: intl.formatMessage(intlMessages.confirmButton),
|
||||
callback: this.onCreateBreakouts,
|
||||
label: isInvitation
|
||||
? intl.formatMessage(intlMessages.invitationConfirm)
|
||||
: intl.formatMessage(intlMessages.confirmButton),
|
||||
callback: isInvitation ? this.onInviteBreakout : this.onCreateBreakouts,
|
||||
}
|
||||
}
|
||||
dismiss={{
|
||||
@ -505,9 +581,7 @@ class BreakoutRoom extends Component {
|
||||
preventClosing={preventClosing}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<p className={styles.subTitle}>
|
||||
{intl.formatMessage(intlMessages.breakoutRoomDesc)}
|
||||
</p>
|
||||
{isInvitation || this.renderTitle()}
|
||||
{isMobileBrowser ? this.renderMobile() : this.renderDesktop()}
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import { styles } from '../styles';
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
toggleSwapLayout: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
restorePresentationLabel: {
|
||||
id: 'app.actionsBar.actionsDropdown.restorePresentationLabel',
|
||||
description: 'Restore Presentation option label',
|
||||
},
|
||||
restorePresentationDesc: {
|
||||
id: 'app.actionsBar.actionsDropdown.restorePresentationDesc',
|
||||
description: 'button to restore presentation after it has been closed',
|
||||
},
|
||||
});
|
||||
|
||||
const PresentationOptionsContainer = ({ intl, toggleSwapLayout }) => (
|
||||
<Button
|
||||
className={styles.button}
|
||||
icon="presentation"
|
||||
label={intl.formatMessage(intlMessages.restorePresentationLabel)}
|
||||
description={intl.formatMessage(intlMessages.restorePresentationDesc)}
|
||||
color="primary"
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
onClick={toggleSwapLayout}
|
||||
id="restore-presentation"
|
||||
/>
|
||||
);
|
||||
|
||||
PresentationOptionsContainer.propTypes = propTypes;
|
||||
export default injectIntl(PresentationOptionsContainer);
|
@ -4,32 +4,21 @@ import { makeCall } from '/imports/ui/services/api';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Breakouts from '/imports/api/breakouts';
|
||||
|
||||
const processOutsideToggleRecording = (e) => {
|
||||
switch (e.data) {
|
||||
case 'c_record': {
|
||||
makeCall('toggleRecording');
|
||||
break;
|
||||
}
|
||||
case 'c_recording_status': {
|
||||
const recordingState = Meetings.findOne({ meetingId: Auth.meetingID }).recordProp.recording;
|
||||
const recordingMessage = recordingState ? 'recordingStarted' : 'recordingStopped';
|
||||
this.window.parent.postMessage({ response: recordingMessage }, '*');
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// console.log(e.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const connectRecordingObserver = () => {
|
||||
// notify on load complete
|
||||
this.window.parent.postMessage({ response: 'readyToConnect' }, '*');
|
||||
const getBreakouts = () => Breakouts.find({ parentMeetingId: Auth.meetingID })
|
||||
.fetch()
|
||||
.sort((a, b) => a.sequence - b.sequence);
|
||||
|
||||
const getUsersNotAssigned = (users) => {
|
||||
const breakouts = getBreakouts();
|
||||
const breakoutUsers = breakouts
|
||||
.reduce((acc, value) => [...acc, ...value.users], [])
|
||||
.map(u => u.userId);
|
||||
return users.filter(u => !breakoutUsers.includes(u.intId));
|
||||
};
|
||||
|
||||
const takePresenterRole = () => makeCall('assignPresenter', Auth.userID);
|
||||
|
||||
export default {
|
||||
connectRecordingObserver: () => connectRecordingObserver(),
|
||||
isUserPresenter: () => Users.findOne({ userId: Auth.userID }).presenter,
|
||||
isUserModerator: () => Users.findOne({ userId: Auth.userID }).moderator,
|
||||
recordSettingsList: () => Meetings.findOne({ meetingId: Auth.meetingID }).recordProp,
|
||||
@ -38,6 +27,9 @@ export default {
|
||||
users: () => Users.find({ connectionStatus: 'online' }).fetch(),
|
||||
hasBreakoutRoom: () => Breakouts.find({ parentMeetingId: Auth.meetingID }).fetch().length > 0,
|
||||
toggleRecording: () => makeCall('toggleRecording'),
|
||||
processOutsideToggleRecording: arg => processOutsideToggleRecording(arg),
|
||||
createBreakoutRoom: (numberOfRooms, durationInMinutes, freeJoin = true, record = false) => makeCall('createBreakoutRoom', numberOfRooms, durationInMinutes, freeJoin, record),
|
||||
sendInvitation: (breakoutId, userId) => makeCall('requestJoinURL', { breakoutId, userId }),
|
||||
getBreakouts,
|
||||
getUsersNotAssigned,
|
||||
takePresenterRole,
|
||||
};
|
||||
|
@ -2,13 +2,15 @@
|
||||
|
||||
.actionsbar,
|
||||
.left,
|
||||
.center {
|
||||
.center,
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.left,
|
||||
.center {
|
||||
.center,
|
||||
.right {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
|
||||
@ -29,6 +31,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
position: absolute;
|
||||
bottom: var(--sm-padding-x);
|
||||
right: var(--sm-padding-x);
|
||||
}
|
||||
|
||||
.centerWithActions {
|
||||
@include mq($xsmall-only) {
|
||||
justify-content: flex-end;
|
||||
@ -57,4 +65,4 @@
|
||||
span:hover {
|
||||
border: 1.5px solid rgba(255,255,255, .5) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,9 @@ import ChatAlertContainer from '../chat/alert/container';
|
||||
import { styles } from './styles';
|
||||
|
||||
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
|
||||
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
userListLabel: {
|
||||
@ -36,7 +39,6 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
fontSize: PropTypes.string,
|
||||
navbar: PropTypes.element,
|
||||
sidebar: PropTypes.element,
|
||||
media: PropTypes.element,
|
||||
@ -49,7 +51,6 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
fontSize: '16px',
|
||||
navbar: null,
|
||||
sidebar: null,
|
||||
media: null,
|
||||
@ -70,13 +71,14 @@ class App extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { locale, fontSize } = this.props;
|
||||
const { locale } = this.props;
|
||||
const BROWSER_RESULTS = browser();
|
||||
const isMobileBrowser = BROWSER_RESULTS.mobile || BROWSER_RESULTS.os.includes('Android');
|
||||
|
||||
Modal.setAppElement('#app');
|
||||
document.getElementsByTagName('html')[0].lang = locale;
|
||||
document.getElementsByTagName('html')[0].style.fontSize = fontSize;
|
||||
document.getElementsByTagName('html')[0].style.fontSize = isMobileBrowser ? MOBILE_FONT_SIZE : DESKTOP_FONT_SIZE;
|
||||
|
||||
const BROWSER_RESULTS = browser();
|
||||
const body = document.getElementsByTagName('body')[0];
|
||||
if (BROWSER_RESULTS && BROWSER_RESULTS.name) {
|
||||
body.classList.add(`browser-${BROWSER_RESULTS.name}`);
|
||||
@ -192,14 +194,14 @@ class App extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
customStyle, customStyleUrl, micsLocked,
|
||||
customStyle, customStyleUrl, openPanel,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<NotificationsBarContainer />
|
||||
<section className={styles.wrapper}>
|
||||
<div className={styles.content}>
|
||||
<div className={openPanel ? styles.content : styles.noPanelContent}>
|
||||
{this.renderNavBar()}
|
||||
{this.renderMedia()}
|
||||
{this.renderActionsBar()}
|
||||
@ -209,7 +211,7 @@ class App extends Component {
|
||||
</section>
|
||||
<PollingContainer />
|
||||
<ModalContainer />
|
||||
{micsLocked ? null : <AudioContainer />}
|
||||
<AudioContainer />
|
||||
<ToastContainer />
|
||||
<ChatAlertContainer />
|
||||
{customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null}
|
||||
|
@ -116,7 +116,6 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
chatIsOpen: Session.equals('openPanel', 'chat'),
|
||||
openPanel: Session.get('openPanel'),
|
||||
userListIsOpen: !Session.equals('openPanel', ''),
|
||||
micsLocked: (currentUserIsLocked && meeting.lockSettingsProp.disableMic),
|
||||
};
|
||||
})(AppContainer)));
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
--bars-padding: calc(var(--lg-padding-x) - .45rem); // -.45 so user-list and chat title is aligned with the presentation title
|
||||
--userlist-handle-width: 5px; // 5px so user-list and chat resize handle render as the same size
|
||||
--poll-pane-min-width: 20em;
|
||||
--panel-margin-left: 0.1em;
|
||||
}
|
||||
|
||||
.main {
|
||||
@ -57,7 +58,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.content, .noPanelContent {
|
||||
@extend %full-page;
|
||||
order: 3;
|
||||
|
||||
@ -89,13 +90,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.content{
|
||||
margin-left: var(--panel-margin-left);
|
||||
}
|
||||
|
||||
.userList {
|
||||
@extend %full-page;
|
||||
@extend %text-elipsis;
|
||||
|
||||
z-index: 2;
|
||||
overflow: visible;
|
||||
overflow: visible;
|
||||
order: 1;
|
||||
|
||||
@include mq($small-only) {
|
||||
@ -123,6 +127,7 @@
|
||||
|
||||
.poll,
|
||||
.breakoutRoom,
|
||||
.note,
|
||||
.chat {
|
||||
@extend %full-page;
|
||||
|
||||
@ -176,7 +181,7 @@
|
||||
order: 2;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
|
||||
margin-left: var(--panel-margin-right);
|
||||
@include mq($portrait) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -44,12 +44,7 @@ export default withModalMounter(withTracker(({ mountModal }) => ({
|
||||
glow: Service.isTalking() && !Service.isMuted(),
|
||||
handleToggleMuteMicrophone: () => Service.toggleMuteMicrophone(),
|
||||
handleJoinAudio: () => {
|
||||
const meeting = Meetings.findOne({ meetingId: Auth.meetingID });
|
||||
const currentUser = Users.findOne({ userId: Auth.userID });
|
||||
const currentUserIsLocked = mapUser(currentUser).isLocked;
|
||||
const micsLocked = (currentUserIsLocked && meeting.lockSettingsProp.disableMic);
|
||||
|
||||
return micsLocked ? Service.joinListenOnly() : mountModal(<AudioModalContainer />);
|
||||
return Service.isConnected() ? Service.joinListenOnly() : mountModal(<AudioModalContainer />);
|
||||
},
|
||||
handleLeaveAudio: () => Service.exitAudio(),
|
||||
}))(AudioControlsContainer));
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ModalBase from '/imports/ui/components/modal/base/component';
|
||||
import Modal from '/imports/ui/components/modal/simple/component';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { styles } from './styles';
|
||||
@ -9,6 +9,7 @@ import AudioSettings from '../audio-settings/component';
|
||||
import EchoTest from '../echo-test/component';
|
||||
import Help from '../help/component';
|
||||
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
@ -97,15 +98,7 @@ class AudioModal extends Component {
|
||||
hasError: false,
|
||||
};
|
||||
|
||||
const {
|
||||
intl,
|
||||
closeModal,
|
||||
joinEchoTest,
|
||||
exitAudio,
|
||||
leaveEchoTest,
|
||||
changeInputDevice,
|
||||
changeOutputDevice,
|
||||
} = props;
|
||||
const { intl } = props;
|
||||
|
||||
this.handleGoToAudioOptions = this.handleGoToAudioOptions.bind(this);
|
||||
this.handleGoToAudioSettings = this.handleGoToAudioSettings.bind(this);
|
||||
@ -114,12 +107,6 @@ class AudioModal extends Component {
|
||||
this.handleJoinMicrophone = this.handleJoinMicrophone.bind(this);
|
||||
this.handleJoinListenOnly = this.handleJoinListenOnly.bind(this);
|
||||
this.skipAudioOptions = this.skipAudioOptions.bind(this);
|
||||
this.closeModal = closeModal;
|
||||
this.joinEchoTest = joinEchoTest;
|
||||
this.exitAudio = exitAudio;
|
||||
this.leaveEchoTest = leaveEchoTest;
|
||||
this.changeInputDevice = changeInputDevice;
|
||||
this.changeOutputDevice = changeOutputDevice;
|
||||
|
||||
this.contents = {
|
||||
echoTest: {
|
||||
@ -142,6 +129,7 @@ class AudioModal extends Component {
|
||||
joinFullAudioImmediately,
|
||||
joinFullAudioEchoTest,
|
||||
forceListenOnlyAttendee,
|
||||
audioLocked,
|
||||
} = this.props;
|
||||
|
||||
if (joinFullAudioImmediately) {
|
||||
@ -152,7 +140,7 @@ class AudioModal extends Component {
|
||||
this.handleGoToEchoTest();
|
||||
}
|
||||
|
||||
if (forceListenOnlyAttendee) {
|
||||
if (forceListenOnlyAttendee || audioLocked) {
|
||||
this.handleJoinListenOnly();
|
||||
}
|
||||
}
|
||||
@ -160,10 +148,11 @@ class AudioModal extends Component {
|
||||
componentWillUnmount() {
|
||||
const {
|
||||
isEchoTest,
|
||||
exitAudio,
|
||||
} = this.props;
|
||||
|
||||
if (isEchoTest) {
|
||||
this.exitAudio();
|
||||
exitAudio();
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,7 +164,8 @@ class AudioModal extends Component {
|
||||
}
|
||||
|
||||
handleGoToAudioSettings() {
|
||||
this.leaveEchoTest().then(() => {
|
||||
const { leaveEchoTest } = this.props;
|
||||
leaveEchoTest().then(() => {
|
||||
this.setState({
|
||||
content: 'settings',
|
||||
});
|
||||
@ -199,13 +189,14 @@ class AudioModal extends Component {
|
||||
const {
|
||||
inputDeviceId,
|
||||
outputDeviceId,
|
||||
joinEchoTest,
|
||||
} = this.props;
|
||||
|
||||
this.setState({
|
||||
hasError: false,
|
||||
});
|
||||
|
||||
return this.joinEchoTest().then(() => {
|
||||
return joinEchoTest().then(() => {
|
||||
console.log(inputDeviceId, outputDeviceId);
|
||||
this.setState({
|
||||
content: 'echoTest',
|
||||
@ -260,10 +251,10 @@ class AudioModal extends Component {
|
||||
|
||||
|
||||
return (
|
||||
isConnecting ||
|
||||
forceListenOnlyAttendee ||
|
||||
joinFullAudioImmediately ||
|
||||
joinFullAudioEchoTest
|
||||
isConnecting
|
||||
|| forceListenOnlyAttendee
|
||||
|| joinFullAudioImmediately
|
||||
|| joinFullAudioEchoTest
|
||||
) && !content && !hasError;
|
||||
}
|
||||
|
||||
@ -276,28 +267,34 @@ class AudioModal extends Component {
|
||||
audioLocked,
|
||||
} = this.props;
|
||||
|
||||
const showMicrophone = forceListenOnlyAttendee || audioLocked;
|
||||
|
||||
return (
|
||||
<span className={styles.audioOptions}>
|
||||
{!forceListenOnlyAttendee ?
|
||||
<Button
|
||||
className={styles.audioBtn}
|
||||
label={intl.formatMessage(intlMessages.microphoneLabel)}
|
||||
icon="unmute"
|
||||
circle
|
||||
size="jumbo"
|
||||
disabled={audioLocked}
|
||||
onClick={skipCheck ? this.handleJoinMicrophone : this.handleGoToEchoTest}
|
||||
/>
|
||||
{!showMicrophone
|
||||
? (
|
||||
<Button
|
||||
className={styles.audioBtn}
|
||||
label={intl.formatMessage(intlMessages.microphoneLabel)}
|
||||
icon="unmute"
|
||||
circle
|
||||
size="jumbo"
|
||||
disabled={audioLocked}
|
||||
onClick={skipCheck ? this.handleJoinMicrophone : this.handleGoToEchoTest}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{listenOnlyMode ?
|
||||
<Button
|
||||
className={styles.audioBtn}
|
||||
label={intl.formatMessage(intlMessages.listenOnlyLabel)}
|
||||
icon="listen"
|
||||
circle
|
||||
size="jumbo"
|
||||
onClick={this.handleJoinListenOnly}
|
||||
/>
|
||||
{listenOnlyMode
|
||||
? (
|
||||
<Button
|
||||
className={styles.audioBtn}
|
||||
label={intl.formatMessage(intlMessages.listenOnlyLabel)}
|
||||
icon="listen"
|
||||
circle
|
||||
size="jumbo"
|
||||
onClick={this.handleJoinListenOnly}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</span>
|
||||
);
|
||||
@ -318,7 +315,8 @@ class AudioModal extends Component {
|
||||
<div className={styles.warning}>!</div>
|
||||
<h4 className={styles.main}>{intl.formatMessage(intlMessages.iOSError)}</h4>
|
||||
<div className={styles.text}>{intl.formatMessage(intlMessages.iOSErrorDescription)}</div>
|
||||
<div className={styles.text}>{intl.formatMessage(intlMessages.iOSErrorRecommendation)}
|
||||
<div className={styles.text}>
|
||||
{intl.formatMessage(intlMessages.iOSErrorRecommendation)}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
@ -326,9 +324,9 @@ class AudioModal extends Component {
|
||||
return (
|
||||
<div className={styles.connecting} role="alert">
|
||||
<span>
|
||||
{!isEchoTest ?
|
||||
intl.formatMessage(intlMessages.connecting) :
|
||||
intl.formatMessage(intlMessages.connectingEchoTest)
|
||||
{!isEchoTest
|
||||
? intl.formatMessage(intlMessages.connecting)
|
||||
: intl.formatMessage(intlMessages.connectingEchoTest)
|
||||
}
|
||||
</span>
|
||||
<span className={styles.connectingAnimation} />
|
||||
@ -354,15 +352,18 @@ class AudioModal extends Component {
|
||||
isEchoTest,
|
||||
inputDeviceId,
|
||||
outputDeviceId,
|
||||
joinEchoTest,
|
||||
changeInputDevice,
|
||||
changeOutputDevice,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AudioSettings
|
||||
handleBack={this.handleGoToAudioOptions}
|
||||
handleRetry={this.handleRetryGoToEchoTest}
|
||||
joinEchoTest={this.joinEchoTest}
|
||||
changeInputDevice={this.changeInputDevice}
|
||||
changeOutputDevice={this.changeOutputDevice}
|
||||
joinEchoTest={joinEchoTest}
|
||||
changeInputDevice={changeInputDevice}
|
||||
changeOutputDevice={changeOutputDevice}
|
||||
isConnecting={isConnecting}
|
||||
isConnected={isConnected}
|
||||
isEchoTest={isEchoTest}
|
||||
@ -385,6 +386,7 @@ class AudioModal extends Component {
|
||||
intl,
|
||||
showPermissionsOvelay,
|
||||
isIOSChrome,
|
||||
closeModal,
|
||||
} = this.props;
|
||||
|
||||
const { content } = this.state;
|
||||
@ -392,40 +394,37 @@ class AudioModal extends Component {
|
||||
return (
|
||||
<span>
|
||||
{showPermissionsOvelay ? <PermissionsOverlay /> : null}
|
||||
<ModalBase
|
||||
<Modal
|
||||
overlayClassName={styles.overlay}
|
||||
className={styles.modal}
|
||||
onRequestClose={this.closeModal}
|
||||
onRequestClose={closeModal}
|
||||
hideBorder
|
||||
>
|
||||
{!this.skipAudioOptions() ?
|
||||
{!this.skipAudioOptions()
|
||||
|
||||
<header
|
||||
data-test="audioModalHeader"
|
||||
className={styles.header}
|
||||
>{
|
||||
isIOSChrome ? null :
|
||||
<h3 className={styles.title}>
|
||||
{content ?
|
||||
this.contents[content].title :
|
||||
intl.formatMessage(intlMessages.audioChoiceLabel)}
|
||||
</h3>
|
||||
? (
|
||||
<header
|
||||
data-test="audioModalHeader"
|
||||
className={styles.header}
|
||||
>
|
||||
{
|
||||
isIOSChrome ? null
|
||||
: (
|
||||
<h3 className={styles.title}>
|
||||
{content
|
||||
? this.contents[content].title
|
||||
: intl.formatMessage(intlMessages.audioChoiceLabel)}
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
data-test="modalBaseCloseButton"
|
||||
className={styles.closeBtn}
|
||||
label={intl.formatMessage(intlMessages.closeLabel)}
|
||||
icon="close"
|
||||
size="md"
|
||||
hideLabel
|
||||
onClick={this.closeModal}
|
||||
/>
|
||||
</header>
|
||||
</header>
|
||||
)
|
||||
: null
|
||||
}
|
||||
<div className={styles.content}>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
</ModalBase>
|
||||
</Modal>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -118,6 +118,7 @@
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 1.3rem;
|
||||
color: var(--color-background);
|
||||
white-space: normal;
|
||||
|
||||
@include mq($small-only) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
.testAudioBtn {
|
||||
--hover-color: #0c5cb2;
|
||||
|
||||
margin: 0 !important;
|
||||
background-color: transparent;
|
||||
color: var(--color-primary);
|
||||
font-weight: normal;
|
||||
@ -16,6 +16,7 @@
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
border: none;
|
||||
background-color: transparent !important;
|
||||
color: var(--hover-color) !important;
|
||||
i {
|
||||
|
@ -65,6 +65,7 @@ class AuthenticatedHandler extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (Session.get('codeError')) return this.changeState(true);
|
||||
AuthenticatedHandler.authenticatedRouteHandler((value, error) => {
|
||||
if (error) AuthenticatedHandler.setError(error);
|
||||
this.changeState(true);
|
||||
|
@ -65,19 +65,35 @@ class BreakoutRoom extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { breakoutRoomUser } = this.props;
|
||||
if (this.state.waiting && !this.state.generated) {
|
||||
const breakoutUser = breakoutRoomUser(this.state.requestedBreakoutId);
|
||||
const {
|
||||
breakoutRoomUser,
|
||||
breakoutRooms,
|
||||
closeBreakoutPanel,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
waiting,
|
||||
generated,
|
||||
requestedBreakoutId,
|
||||
} = this.state;
|
||||
|
||||
if (breakoutRooms.length <= 0) closeBreakoutPanel();
|
||||
|
||||
if (waiting && !generated) {
|
||||
const breakoutUser = breakoutRoomUser(requestedBreakoutId);
|
||||
|
||||
if (!breakoutUser) return;
|
||||
if (breakoutUser.redirectToHtml5JoinURL !== '') {
|
||||
_.delay(() => this.setState({ generated: true, waiting: false }), 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getBreakoutURL(breakoutId) {
|
||||
const { requestJoinURL, breakoutRoomUser } = this.props;
|
||||
const { waiting } = this.state;
|
||||
const hasUser = breakoutRoomUser(breakoutId);
|
||||
if (!hasUser && !this.state.waiting) {
|
||||
if (!hasUser && !waiting) {
|
||||
this.setState(
|
||||
{ waiting: true, requestedBreakoutId: breakoutId },
|
||||
() => requestJoinURL(breakoutId),
|
||||
@ -96,6 +112,7 @@ class BreakoutRoom extends Component {
|
||||
transferToBreakout(breakoutId);
|
||||
this.setState({ joinedAudioOnly: true, breakoutId });
|
||||
}
|
||||
|
||||
returnBackToMeeeting(breakoutId) {
|
||||
const { transferUserToMeeting, meetingId } = this.props;
|
||||
transferUserToMeeting(breakoutId, meetingId);
|
||||
@ -119,9 +136,9 @@ class BreakoutRoom extends Component {
|
||||
|
||||
const moderatorJoinedAudio = isMicrophoneUser && isModerator;
|
||||
const disable = waiting && requestedBreakoutId !== breakoutId;
|
||||
const audioAction = joinedAudioOnly ?
|
||||
() => this.returnBackToMeeeting(breakoutId) :
|
||||
() => this.transferUserToBreakoutRoom(breakoutId);
|
||||
const audioAction = joinedAudioOnly
|
||||
? () => this.returnBackToMeeeting(breakoutId)
|
||||
: () => this.transferUserToBreakoutRoom(breakoutId);
|
||||
return (
|
||||
<div className={styles.breakoutActions}>
|
||||
<Button
|
||||
@ -133,18 +150,17 @@ class BreakoutRoom extends Component {
|
||||
className={styles.joinButton}
|
||||
/>
|
||||
{
|
||||
moderatorJoinedAudio ?
|
||||
[
|
||||
moderatorJoinedAudio
|
||||
? [
|
||||
('|'),
|
||||
(
|
||||
<Button
|
||||
label={
|
||||
moderatorJoinedAudio &&
|
||||
stateBreakoutId === breakoutId &&
|
||||
joinedAudioOnly
|
||||
?
|
||||
intl.formatMessage(intlMessages.breakoutReturnAudio) :
|
||||
intl.formatMessage(intlMessages.breakoutJoinAudio)
|
||||
moderatorJoinedAudio
|
||||
&& stateBreakoutId === breakoutId
|
||||
&& joinedAudioOnly
|
||||
? intl.formatMessage(intlMessages.breakoutReturnAudio)
|
||||
: intl.formatMessage(intlMessages.breakoutJoinAudio)
|
||||
}
|
||||
className={styles.button}
|
||||
onClick={audioAction}
|
||||
@ -163,10 +179,15 @@ class BreakoutRoom extends Component {
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
waiting,
|
||||
requestedBreakoutId,
|
||||
} = this.state;
|
||||
|
||||
const roomItems = breakoutRooms.map(item => (
|
||||
<div className={styles.content} key={`breakoutRoomList-${item.breakoutId}`}>
|
||||
<span>{intl.formatMessage(intlMessages.breakoutRoom, item.sequence.toString())}</span>
|
||||
{this.state.waiting && this.state.requestedBreakoutId === item.breakoutId ? (
|
||||
{waiting && requestedBreakoutId === item.breakoutId ? (
|
||||
<span>
|
||||
{intl.formatMessage(intlMessages.generatingURL)}
|
||||
<span className={styles.connectingAnimation} />
|
||||
@ -192,13 +213,12 @@ class BreakoutRoom extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl, endAllBreakouts, breakoutRooms, isModerator, closeBreakoutPanel,
|
||||
intl, endAllBreakouts, isModerator, closeBreakoutPanel,
|
||||
} = this.props;
|
||||
if (breakoutRooms.length <= 0) return null;
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.header} role="button" onClick={closeBreakoutPanel} >
|
||||
<span >
|
||||
<span>
|
||||
<Icon iconName="left_arrow" />
|
||||
{intl.formatMessage(intlMessages.breakoutTitle)}
|
||||
</span>
|
||||
@ -206,8 +226,8 @@ class BreakoutRoom extends Component {
|
||||
{this.renderBreakoutRooms()}
|
||||
{this.renderDuration()}
|
||||
{
|
||||
isModerator ?
|
||||
(
|
||||
isModerator
|
||||
? (
|
||||
<Button
|
||||
color="primary"
|
||||
size="lg"
|
||||
|
@ -260,7 +260,7 @@
|
||||
}
|
||||
|
||||
.circle {
|
||||
--btn-sm-padding-x: calc(var(--lg-padding-x) / 2.75);
|
||||
--btn-sm-padding-x: calc(var(--sm-padding-x) / 2.75);
|
||||
--btn-md-padding-x: calc(var(--md-padding-x) / 2.75);
|
||||
--btn-lg-padding-x: calc(var(--lg-padding-x) / 2.75);
|
||||
--btn-jumbo-padding-x: calc(var(--jumbo-padding-x) / 2.75);
|
||||
|
@ -1,41 +1,33 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
play: PropTypes.bool.isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
class ChatAudioAlert extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.audio = new Audio(`${Meteor.settings.public.app.basename}/resources/sounds/notify.mp3`);
|
||||
|
||||
this.handleAudioLoaded = this.handleAudioLoaded.bind(this);
|
||||
this.playAudio = this.playAudio.bind(this);
|
||||
this.componentDidUpdate = _.debounce(this.playAudio, 2000);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.audio.addEventListener('loadedmetadata', this.handleAudioLoaded);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.count > this.props.count;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.audio.removeEventListener('loadedmetadata', this.handleAudioLoaded);
|
||||
}
|
||||
|
||||
handleAudioLoaded() {
|
||||
this.componentDidUpdate = _.debounce(this.playAudio, this.audio.duration * 1000);
|
||||
this.componentDidUpdate = this.playAudio;
|
||||
}
|
||||
|
||||
playAudio() {
|
||||
if (!this.props.play) return;
|
||||
|
||||
const { play } = this.props;
|
||||
if (!play) return;
|
||||
this.audio.play();
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,10 @@ import Service from '../service';
|
||||
import { styles } from '../styles';
|
||||
|
||||
const propTypes = {
|
||||
disableNotify: PropTypes.bool.isRequired,
|
||||
openChats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
disableAudio: PropTypes.bool.isRequired,
|
||||
pushAlertDisabled: PropTypes.bool.isRequired,
|
||||
activeChats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
audioAlertDisabled: PropTypes.bool.isRequired,
|
||||
joinTimestamp: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -34,51 +35,105 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const PUBLIC_KEY = 'public';
|
||||
const PRIVATE_KEY = 'private';
|
||||
const ALERT_INTERVAL = 5000; // 5 seconds
|
||||
const ALERT_DURATION = 4000; // 4 seconds
|
||||
|
||||
class ChatAlert extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
notified: Service.getNotified(PRIVATE_KEY),
|
||||
publicNotified: Service.getNotified(PUBLIC_KEY),
|
||||
alertEnabledTimestamp: props.joinTimestamp,
|
||||
lastAlertTimestampByChat: {},
|
||||
pendingNotificationsByChat: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
openChats,
|
||||
disableNotify,
|
||||
pushAlertDisabled,
|
||||
activeChats,
|
||||
joinTimestamp,
|
||||
} = this.props;
|
||||
|
||||
if (nextProps.disableNotify === false && disableNotify === true) {
|
||||
const loadMessages = {};
|
||||
openChats
|
||||
.forEach((c) => {
|
||||
loadMessages[c.id] = c.unreadCounter;
|
||||
});
|
||||
this.setState({ notified: loadMessages });
|
||||
const {
|
||||
alertEnabledTimestamp,
|
||||
lastAlertTimestampByChat,
|
||||
pendingNotificationsByChat,
|
||||
} = this.state;
|
||||
// Avoid alerting messages received before enabling alerts
|
||||
if (prevProps.pushAlertDisabled && !pushAlertDisabled) {
|
||||
const newAlertEnabledTimestamp = Service.getLastMessageTimestampFromChatList(activeChats);
|
||||
this.setAlertEnabledTimestamp(newAlertEnabledTimestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
const notifiedToClear = {};
|
||||
openChats
|
||||
.filter(c => c.unreadCounter === 0)
|
||||
.forEach((c) => {
|
||||
notifiedToClear[c.id] = 0;
|
||||
|
||||
// Keep track of messages that was not alerted yet
|
||||
const unalertedMessagesByChatId = {};
|
||||
|
||||
activeChats
|
||||
.filter(chat => chat.id !== Session.get('idChatOpen'))
|
||||
.filter(chat => chat.unreadCounter > 0)
|
||||
.forEach((chat) => {
|
||||
const chatId = (chat.id === 'public') ? 'MAIN-PUBLIC-GROUP-CHAT' : chat.id;
|
||||
const thisChatUnreadMessages = UnreadMessages.getUnreadMessages(chatId);
|
||||
|
||||
unalertedMessagesByChatId[chatId] = thisChatUnreadMessages.filter((msg) => {
|
||||
const messageChatId = (msg.chatId === 'MAIN-PUBLIC-GROUP-CHAT') ? msg.chatId : msg.sender;
|
||||
const retorno = (msg
|
||||
&& msg.timestamp > alertEnabledTimestamp
|
||||
&& msg.timestamp > joinTimestamp
|
||||
&& msg.timestamp > (lastAlertTimestampByChat[messageChatId] || 0)
|
||||
);
|
||||
return retorno;
|
||||
});
|
||||
|
||||
if (!unalertedMessagesByChatId[chatId].length) delete unalertedMessagesByChatId[chatId];
|
||||
});
|
||||
|
||||
this.setState(({ notified }) => ({
|
||||
notified: {
|
||||
...notified,
|
||||
...notifiedToClear,
|
||||
},
|
||||
}), () => {
|
||||
Service.setNotified(PRIVATE_KEY, this.state.notified);
|
||||
const lastUnalertedMessageTimestampByChat = {};
|
||||
Object.keys(unalertedMessagesByChatId).forEach((chatId) => {
|
||||
lastUnalertedMessageTimestampByChat[chatId] = unalertedMessagesByChatId[chatId]
|
||||
.reduce(Service.maxTimestampReducer, 0);
|
||||
});
|
||||
|
||||
// Keep track of chats that need to be alerted now (considering alert interval)
|
||||
const chatsWithPendingAlerts = Object.keys(lastUnalertedMessageTimestampByChat)
|
||||
.filter(chatId => lastUnalertedMessageTimestampByChat[chatId]
|
||||
> ((lastAlertTimestampByChat[chatId] || 0) + ALERT_INTERVAL)
|
||||
&& !(chatId in pendingNotificationsByChat));
|
||||
|
||||
if (!chatsWithPendingAlerts.length) return;
|
||||
|
||||
const newPendingNotificationsByChat = Object.assign({},
|
||||
...chatsWithPendingAlerts.map(chatId => ({ [chatId]: unalertedMessagesByChatId[chatId] })));
|
||||
|
||||
// Mark messages as alerted
|
||||
const newLastAlertTimestampByChat = { ...lastAlertTimestampByChat };
|
||||
|
||||
chatsWithPendingAlerts.forEach(
|
||||
(chatId) => {
|
||||
newLastAlertTimestampByChat[chatId] = lastUnalertedMessageTimestampByChat[chatId];
|
||||
},
|
||||
);
|
||||
|
||||
if (!pushAlertDisabled) {
|
||||
this.setChatMessagesState(newPendingNotificationsByChat, newLastAlertTimestampByChat);
|
||||
}
|
||||
}
|
||||
|
||||
setAlertEnabledTimestamp(newAlertEnabledTimestamp) {
|
||||
const { alertEnabledTimestamp } = this.state;
|
||||
if (newAlertEnabledTimestamp > 0 && alertEnabledTimestamp !== newAlertEnabledTimestamp) {
|
||||
this.setState({ alertEnabledTimestamp: newAlertEnabledTimestamp });
|
||||
}
|
||||
}
|
||||
|
||||
setChatMessagesState(pendingNotificationsByChat, lastAlertTimestampByChat) {
|
||||
this.setState({ pendingNotificationsByChat, lastAlertTimestampByChat });
|
||||
}
|
||||
|
||||
|
||||
mapContentText(message) {
|
||||
const {
|
||||
intl,
|
||||
@ -86,12 +141,13 @@ class ChatAlert extends PureComponent {
|
||||
const contentMessage = message
|
||||
.map((content) => {
|
||||
if (content.text === 'PUBLIC_CHAT_CLEAR') return intl.formatMessage(intlMessages.publicChatClear);
|
||||
/* this code is to remove html tags that come in the server's messangens */
|
||||
/* this code is to remove html tags that come in the server's messages */
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.text;
|
||||
const textWithoutTag = tempDiv.innerText;
|
||||
return textWithoutTag;
|
||||
});
|
||||
|
||||
return contentMessage;
|
||||
}
|
||||
|
||||
@ -102,155 +158,64 @@ class ChatAlert extends PureComponent {
|
||||
<div className={styles.contentMessage}>
|
||||
{
|
||||
this.mapContentText(message)
|
||||
.reduce((acc, text) => [...acc, (<br />), text], []).splice(1)
|
||||
.reduce((acc, text) => [...acc, (<br key={_.uniqueId('br_')} />), text], [])
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
notifyPrivateChat() {
|
||||
const {
|
||||
disableNotify,
|
||||
openChats,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
if (disableNotify) return;
|
||||
|
||||
const hasUnread = ({ unreadCounter }) => unreadCounter > 0;
|
||||
const isNotNotified = ({ id, unreadCounter }) => unreadCounter !== this.state.notified[id];
|
||||
const isPrivate = ({ id }) => id !== PUBLIC_KEY;
|
||||
const thisChatClosed = ({ id }) => !Session.equals('idChatOpen', id);
|
||||
|
||||
const chatsNotify = openChats
|
||||
.filter(hasUnread)
|
||||
.filter(isNotNotified)
|
||||
.filter(isPrivate)
|
||||
.filter(thisChatClosed)
|
||||
.map(({
|
||||
id,
|
||||
name,
|
||||
unreadCounter,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
name,
|
||||
unreadCounter,
|
||||
id,
|
||||
message: intl.formatMessage(intlMessages.appToastChatPrivate),
|
||||
}));
|
||||
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
chatsNotify.map(({ id, message, name }) => {
|
||||
const getChatmessages = UnreadMessages.getUnreadMessages(id)
|
||||
.filter(({ fromTime, fromUserId }) => fromTime > (this.state.notified[fromUserId] || 0));
|
||||
|
||||
const reduceMessages = Service
|
||||
.reduceAndMapGroupMessages(getChatmessages);
|
||||
|
||||
if (!reduceMessages.length) return null;
|
||||
|
||||
const flatMessages = _.flatten(reduceMessages
|
||||
.map(msg => this.createMessage(name, msg.content)));
|
||||
const limitingMessages = flatMessages;
|
||||
|
||||
return (<ChatPushAlert
|
||||
key={_.uniqueId('id-')}
|
||||
chatId={id}
|
||||
content={limitingMessages}
|
||||
message={<span >{message}</span>}
|
||||
onOpen={() => {
|
||||
this.setState(({ notified }) => ({
|
||||
notified: {
|
||||
...notified,
|
||||
[id]: new Date().getTime(),
|
||||
},
|
||||
}), () => {
|
||||
Service.setNotified(PRIVATE_KEY, this.state.notified);
|
||||
});
|
||||
}}
|
||||
/>);
|
||||
})
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
notifyPublicChat() {
|
||||
const {
|
||||
publicUserId,
|
||||
intl,
|
||||
disableNotify,
|
||||
} = this.props;
|
||||
|
||||
const publicUnread = UnreadMessages.getUnreadMessages(publicUserId);
|
||||
const publicUnreadReduced = Service.reduceAndMapGroupMessages(publicUnread);
|
||||
|
||||
if (disableNotify) return;
|
||||
if (!Service.hasUnreadMessages(publicUserId)) return;
|
||||
if (Session.equals('idChatOpen', PUBLIC_KEY)) return;
|
||||
|
||||
const checkIfBeenNotified = ({ sender, time }) =>
|
||||
time > (this.state.publicNotified[sender.id] || 0);
|
||||
|
||||
const chatsNotify = publicUnreadReduced
|
||||
.map(msg => ({
|
||||
...msg,
|
||||
sender: {
|
||||
name: msg.sender ? msg.sender.name : intl.formatMessage(intlMessages.appToastChatSystem),
|
||||
...msg.sender,
|
||||
},
|
||||
}))
|
||||
.filter(checkIfBeenNotified);
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
chatsNotify.map(({ sender, time, content }) =>
|
||||
(<ChatPushAlert
|
||||
key={time}
|
||||
chatId={PUBLIC_KEY}
|
||||
name={sender.name}
|
||||
message={
|
||||
<span >
|
||||
{ intl.formatMessage(intlMessages.appToastChatPublic) }
|
||||
</span>
|
||||
}
|
||||
content={this.createMessage(sender.name, content)}
|
||||
onOpen={() => {
|
||||
this.setState(({ notified, publicNotified }) => ({
|
||||
...notified,
|
||||
publicNotified: {
|
||||
...publicNotified,
|
||||
[sender.id]: time,
|
||||
},
|
||||
}), () => {
|
||||
Service.setNotified(PUBLIC_KEY, this.state.publicNotified);
|
||||
});
|
||||
}}
|
||||
/>))
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
openChats,
|
||||
disableAudio,
|
||||
audioAlertDisabled,
|
||||
pushAlertDisabled,
|
||||
intl,
|
||||
} = this.props;
|
||||
const unreadMessagesCount = openChats
|
||||
.map(chat => chat.unreadCounter)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
const shouldPlayAudio = !disableAudio && unreadMessagesCount > 0;
|
||||
const {
|
||||
pendingNotificationsByChat,
|
||||
} = this.state;
|
||||
|
||||
const shouldPlay = Object.keys(pendingNotificationsByChat).length > 0;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<ChatAudioAlert play={shouldPlayAudio} count={unreadMessagesCount} />
|
||||
{ this.notifyPublicChat() }
|
||||
{ this.notifyPrivateChat() }
|
||||
{!audioAlertDisabled ? <ChatAudioAlert play={shouldPlay} /> : null}
|
||||
{
|
||||
!pushAlertDisabled
|
||||
? Object.keys(pendingNotificationsByChat)
|
||||
.map((chatId) => {
|
||||
// Only display the latest group of messages (up to 5 messages)
|
||||
const reducedMessage = Service
|
||||
.reduceAndMapGroupMessages(pendingNotificationsByChat[chatId].slice(-5)).pop();
|
||||
|
||||
if (!reducedMessage) return null;
|
||||
|
||||
const content = this
|
||||
.createMessage(reducedMessage.sender.name, reducedMessage.content);
|
||||
|
||||
return (
|
||||
<ChatPushAlert
|
||||
key={chatId}
|
||||
chatId={chatId}
|
||||
content={content}
|
||||
title={
|
||||
(chatId === 'MAIN-PUBLIC-GROUP-CHAT')
|
||||
? <span>{intl.formatMessage(intlMessages.appToastChatPublic)}</span>
|
||||
: <span>{intl.formatMessage(intlMessages.appToastChatPrivate)}</span>
|
||||
}
|
||||
onOpen={
|
||||
() => {
|
||||
let pendingNotifications = pendingNotificationsByChat;
|
||||
delete pendingNotifications[chatId];
|
||||
pendingNotifications = { ...pendingNotifications };
|
||||
this.setState({ pendingNotificationsByChat: pendingNotifications });
|
||||
}}
|
||||
alertDuration={ALERT_DURATION}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: null
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ import { withTracker } from 'meteor/react-meteor-data';
|
||||
import UserListService from '/imports/ui/components/user-list/service';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import ChatAlert from './component';
|
||||
import ChatService from '/imports/ui/components/chat/service.js';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Users from '/imports/api/users';
|
||||
|
||||
const ChatAlertContainer = props => (
|
||||
<ChatAlert {...props} />
|
||||
@ -10,12 +13,13 @@ const ChatAlertContainer = props => (
|
||||
|
||||
export default withTracker(() => {
|
||||
const AppSettings = Settings.application;
|
||||
const openChats = UserListService.getOpenChats();
|
||||
|
||||
const activeChats = UserListService.getActiveChats();
|
||||
const loginTime = Users.findOne({ userId: Auth.userID }).loginTime;
|
||||
return {
|
||||
disableAudio: !AppSettings.chatAudioAlerts,
|
||||
disableNotify: !AppSettings.chatPushAlerts,
|
||||
openChats,
|
||||
audioAlertDisabled: !AppSettings.chatAudioAlerts,
|
||||
pushAlertDisabled: !AppSettings.chatPushAlerts,
|
||||
activeChats,
|
||||
publicUserId: Meteor.settings.public.chat.public_group_id,
|
||||
joinTimestamp: loginTime,
|
||||
};
|
||||
})(memo(ChatAlertContainer));
|
||||
|
@ -4,37 +4,44 @@ import _ from 'lodash';
|
||||
import injectNotify from '/imports/ui/components/toast/inject-notify/component';
|
||||
import { Session } from 'meteor/session';
|
||||
|
||||
const ALERT_INTERVAL = 2000; // 2 seconds
|
||||
const ALERT_LIFETIME = 4000; // 4 seconds
|
||||
|
||||
const propTypes = {
|
||||
notify: PropTypes.func.isRequired,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
chatId: PropTypes.string.isRequired,
|
||||
message: PropTypes.node.isRequired,
|
||||
title: PropTypes.node.isRequired,
|
||||
content: PropTypes.node.isRequired,
|
||||
alertDuration: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
class ChatPushAlert extends PureComponent {
|
||||
static link(message, chatId) {
|
||||
static link(title, chatId) {
|
||||
let chat = chatId;
|
||||
|
||||
if (chat === 'MAIN-PUBLIC-GROUP-CHAT') {
|
||||
chat = 'public';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={chatId}
|
||||
role="button"
|
||||
aria-label={message}
|
||||
aria-label={title}
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
Session.set('openPanel', 'chat');
|
||||
Session.set('idChatOpen', chatId);
|
||||
Session.set('idChatOpen', chat);
|
||||
}}
|
||||
onKeyPress={() => null}
|
||||
>
|
||||
{ message }
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.showNotify = _.debounce(this.showNotify.bind(this), ALERT_INTERVAL);
|
||||
this.showNotify = this.showNotify.bind(this);
|
||||
|
||||
this.componentDidMount = this.showNotify;
|
||||
this.componentDidUpdate = this.showNotify;
|
||||
@ -45,15 +52,16 @@ class ChatPushAlert extends PureComponent {
|
||||
notify,
|
||||
onOpen,
|
||||
chatId,
|
||||
message,
|
||||
title,
|
||||
content,
|
||||
alertDuration,
|
||||
} = this.props;
|
||||
|
||||
return notify(
|
||||
ChatPushAlert.link(message, chatId),
|
||||
ChatPushAlert.link(title, chatId),
|
||||
'info',
|
||||
'chat',
|
||||
{ onOpen, autoClose: ALERT_LIFETIME },
|
||||
{ onOpen, autoClose: alertDuration },
|
||||
ChatPushAlert.link(content, chatId),
|
||||
true,
|
||||
);
|
||||
|
@ -56,6 +56,7 @@ const Chat = (props) => {
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
Session.set('idChatOpen', '');
|
||||
Session.set('openPanel', 'userlist');
|
||||
}}
|
||||
aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })}
|
||||
@ -75,6 +76,7 @@ const Chat = (props) => {
|
||||
hideLabel
|
||||
onClick={() => {
|
||||
actions.handleClosePrivateChat(chatID);
|
||||
Session.set('idChatOpen', '');
|
||||
Session.set('openPanel', 'userlist');
|
||||
}}
|
||||
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
|
||||
|
@ -59,7 +59,7 @@ export default injectIntl(withTracker(({ intl }) => {
|
||||
|
||||
messages = ChatService.getPublicGroupMessages();
|
||||
|
||||
const time = user.logTime;
|
||||
const time = user.loginTime;
|
||||
const welcomeId = `welcome-msg-${time}`;
|
||||
|
||||
const welcomeMsg = {
|
||||
|
@ -118,6 +118,7 @@ class MessageForm extends PureComponent {
|
||||
const { message } = this.state;
|
||||
let msg = message.trim();
|
||||
|
||||
|
||||
if (disabled
|
||||
|| msg.length === 0
|
||||
|| msg.length < minMessageLength
|
||||
@ -132,11 +133,13 @@ class MessageForm extends PureComponent {
|
||||
div.appendChild(document.createTextNode(msg));
|
||||
msg = div.innerHTML;
|
||||
|
||||
return handleSendMessage(msg)
|
||||
.then(() => this.setState({
|
||||
return (
|
||||
handleSendMessage(msg),
|
||||
this.setState({
|
||||
message: '',
|
||||
hasErrors: false,
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -184,7 +187,7 @@ class MessageForm extends PureComponent {
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
{ hasErrors ? <span id="message-input-error">{error}</span> : null }
|
||||
{hasErrors ? <span id="message-input-error">{error}</span> : null}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -24,6 +24,7 @@ const eventsToBeBound = [
|
||||
];
|
||||
|
||||
const isElementInViewport = (el) => {
|
||||
if (!el) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const prefetchHeight = 125;
|
||||
|
||||
@ -87,8 +88,8 @@ export default class MessageListItem extends Component {
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if(!this.props.scrollArea && nextProps.scrollArea) return true;
|
||||
else return !nextState.preventRender && nextState.pendingChanges;
|
||||
if (!this.props.scrollArea && nextProps.scrollArea) return true;
|
||||
return !nextState.preventRender && nextState.pendingChanges;
|
||||
}
|
||||
|
||||
renderSystemMessage() {
|
||||
@ -120,7 +121,7 @@ export default class MessageListItem extends Component {
|
||||
const {
|
||||
user,
|
||||
messages,
|
||||
time
|
||||
time,
|
||||
} = this.props;
|
||||
|
||||
const dateTime = new Date(time);
|
||||
|
@ -18,13 +18,14 @@ const eventsToBeBound = [
|
||||
];
|
||||
|
||||
const isElementInViewport = (el) => {
|
||||
if (!el) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
rect.top >= 0
|
||||
&& rect.left >= 0
|
||||
&& rect.bottom <= (window.innerHeight || document.documentElement.clientHeight)
|
||||
&& rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
};
|
||||
|
||||
@ -45,7 +46,7 @@ export default class MessageListItem extends PureComponent {
|
||||
|
||||
if (isElementInViewport(node)) {
|
||||
this.props.handleReadMessage(this.props.time);
|
||||
if(scrollArea) {
|
||||
if (scrollArea) {
|
||||
eventsToBeBound.forEach(
|
||||
e => scrollArea.removeEventListener(e, this.handleMessageInViewport),
|
||||
);
|
||||
@ -76,7 +77,7 @@ export default class MessageListItem extends PureComponent {
|
||||
(e) => { scrollArea.addEventListener(e, this.handleMessageInViewport, false); },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.listenToUnreadMessages();
|
||||
@ -114,7 +115,6 @@ export default class MessageListItem extends PureComponent {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MessageListItem.propTypes = propTypes;
|
||||
|
@ -18,6 +18,7 @@ const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
|
||||
const PRIVATE_CHAT_TYPE = CHAT_CONFIG.type_private;
|
||||
const PUBLIC_CHAT_USER_ID = CHAT_CONFIG.system_userid;
|
||||
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.system_messages_keys.chat_clear;
|
||||
|
||||
const ScrollCollection = new Mongo.Collection(null);
|
||||
|
||||
@ -91,16 +92,13 @@ const reduceGroupMessages = (previous, current) => {
|
||||
return previous.concat(currentMessage);
|
||||
};
|
||||
|
||||
const reduceAndMapGroupMessages = messages =>
|
||||
(messages.reduce(reduceGroupMessages, []).map(mapGroupMessage));
|
||||
const reduceAndMapGroupMessages = messages => (messages
|
||||
.reduce(reduceGroupMessages, []).map(mapGroupMessage));
|
||||
|
||||
const getPublicGroupMessages = () => {
|
||||
const publicGroupMessages = GroupChatMsg.find({
|
||||
chatId: PUBLIC_GROUP_CHAT_ID,
|
||||
}, {
|
||||
sort: ['timestamp'],
|
||||
}).fetch();
|
||||
|
||||
}, { sort: ['timestamp'] }).fetch();
|
||||
return publicGroupMessages;
|
||||
};
|
||||
|
||||
@ -122,9 +120,7 @@ const getPrivateGroupMessages = () => {
|
||||
|
||||
messages = GroupChatMsg.find({
|
||||
chatId,
|
||||
}, {
|
||||
sort: ['timestamp'],
|
||||
}).fetch();
|
||||
}, { sort: ['timestamp'] }).fetch();
|
||||
}
|
||||
|
||||
return reduceAndMapGroupMessages(messages, []);
|
||||
@ -140,8 +136,8 @@ const isChatLocked = (receiverID) => {
|
||||
const isPubChatLocked = meeting.lockSettingsProp.disablePubChat;
|
||||
const isPrivChatLocked = meeting.lockSettingsProp.disablePrivChat;
|
||||
|
||||
return mapUser(user).isLocked &&
|
||||
((isPublic && isPubChatLocked) || (!isPublic && isPrivChatLocked));
|
||||
return mapUser(user).isLocked
|
||||
&& ((isPublic && isPubChatLocked) || (!isPublic && isPrivChatLocked));
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -205,11 +201,10 @@ const getScrollPosition = (receiverID) => {
|
||||
return scroll.position;
|
||||
};
|
||||
|
||||
const updateScrollPosition =
|
||||
position => ScrollCollection.upsert(
|
||||
{ receiver: Session.get('idChatOpen') },
|
||||
{ $set: { position } },
|
||||
);
|
||||
const updateScrollPosition = position => ScrollCollection.upsert(
|
||||
{ receiver: Session.get('idChatOpen') },
|
||||
{ $set: { position } },
|
||||
);
|
||||
|
||||
const updateUnreadMessage = (timestamp) => {
|
||||
const chatID = Session.get('idChatOpen') || PUBLIC_CHAT_ID;
|
||||
@ -248,8 +243,27 @@ const htmlDecode = (input) => {
|
||||
};
|
||||
|
||||
// Export the chat as [Hour:Min] user: message
|
||||
const exportChat = messageList => (
|
||||
messageList.map((message) => {
|
||||
const exportChat = (messageList) => {
|
||||
const { welcomeProp } = getMeeting();
|
||||
const { loginTime } = getUser(Auth.userID);
|
||||
const { welcomeMsg } = welcomeProp;
|
||||
|
||||
const clearMessage = messageList.filter(message => message.message === PUBLIC_CHAT_CLEAR);
|
||||
|
||||
const hasClearMessage = clearMessage.length;
|
||||
|
||||
if (!hasClearMessage || (hasClearMessage && clearMessage[0].timestamp < loginTime)) {
|
||||
messageList.push({
|
||||
timestamp: loginTime,
|
||||
message: welcomeMsg,
|
||||
type: SYSTEM_CHAT_TYPE,
|
||||
sender: PUBLIC_CHAT_USER_ID,
|
||||
});
|
||||
}
|
||||
|
||||
messageList.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
return messageList.map((message) => {
|
||||
const date = new Date(message.timestamp);
|
||||
const hour = date.getHours().toString().padStart(2, 0);
|
||||
const min = date.getMinutes().toString().padStart(2, 0);
|
||||
@ -259,31 +273,41 @@ const exportChat = messageList => (
|
||||
}
|
||||
const userName = message.sender === PUBLIC_CHAT_USER_ID ? '' : `${getUser(message.sender).name} :`;
|
||||
return `${hourMin} ${userName} ${htmlDecode(message.message)}`;
|
||||
}).join('\n')
|
||||
);
|
||||
}).join('\n');
|
||||
};
|
||||
|
||||
const setNotified = (chatType, item) => {
|
||||
const notified = Storage.getItem('notified');
|
||||
const key = 'notified';
|
||||
const userChat = { [chatType]: item };
|
||||
if (notified) {
|
||||
Storage.setItem(key, {
|
||||
...notified,
|
||||
...userChat,
|
||||
});
|
||||
return;
|
||||
const getUnreadMessagesFromChatId = chatId => UnreadMessages.getUnreadMessages(chatId);
|
||||
|
||||
const getAllMessages = (chatID) => {
|
||||
const filter = {
|
||||
sender: { $ne: Auth.userID },
|
||||
};
|
||||
if (chatID === PUBLIC_GROUP_CHAT_ID) {
|
||||
filter.chatId = { $eq: chatID };
|
||||
} else {
|
||||
const privateChat = GroupChat.findOne({ users: { $all: [chatID, Auth.userID] } });
|
||||
|
||||
filter.chatId = { $ne: PUBLIC_GROUP_CHAT_ID };
|
||||
|
||||
if (privateChat) {
|
||||
filter.chatId = privateChat.chatId;
|
||||
}
|
||||
}
|
||||
Storage.setItem(key, {
|
||||
...userChat,
|
||||
});
|
||||
const messages = GroupChatMsg.find(filter).fetch();
|
||||
return messages;
|
||||
};
|
||||
|
||||
const getNotified = (chat) => {
|
||||
const key = 'notified';
|
||||
const notified = Storage.getItem(key);
|
||||
if (notified) return notified[chat] || {};
|
||||
return {};
|
||||
};
|
||||
const getlastMessage = lastMessages => lastMessages.sort((a,
|
||||
b) => a.timestamp - b.timestamp).pop();
|
||||
|
||||
const maxTimestampReducer = (max, el) => ((el.timestamp > max) ? el.timestamp : max);
|
||||
|
||||
const maxNumberReducer = (max, el) => ((el > max) ? el : max);
|
||||
|
||||
const getLastMessageTimestampFromChatList = activeChats => activeChats
|
||||
.map(chat => ((chat.id === 'public') ? 'MAIN-PUBLIC-GROUP-CHAT' : chat.id))
|
||||
.map(chatId => getAllMessages(chatId).reduce(maxTimestampReducer, 0))
|
||||
.reduce(maxNumberReducer, 0);
|
||||
|
||||
export default {
|
||||
reduceAndMapGroupMessages,
|
||||
@ -302,6 +326,10 @@ export default {
|
||||
removeFromClosedChatsSession,
|
||||
exportChat,
|
||||
clearPublicChatHistory,
|
||||
setNotified,
|
||||
getNotified,
|
||||
getlastMessage,
|
||||
getUnreadMessagesFromChatId,
|
||||
getAllMessages,
|
||||
maxTimestampReducer,
|
||||
maxNumberReducer,
|
||||
getLastMessageTimestampFromChatList,
|
||||
};
|
||||
|
@ -1,9 +1,17 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
||||
import { styles } from './styles.scss';
|
||||
|
||||
class ClosedCaptions extends Component {
|
||||
const intlMessages = defineMessages({
|
||||
noLocaleSelected: {
|
||||
id: 'app.submenu.closedCaptions.noLocaleSelected',
|
||||
description: 'label for selected language for closed captions',
|
||||
},
|
||||
});
|
||||
|
||||
class ClosedCaptions extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@ -25,13 +33,14 @@ class ClosedCaptions extends Component {
|
||||
}
|
||||
|
||||
renderCaptions(caption) {
|
||||
const { fontFamily, fontSize, fontColor } = this.props;
|
||||
const text = caption.captions;
|
||||
const captionStyles = {
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
fontFamily: this.props.fontFamily,
|
||||
fontSize: this.props.fontSize,
|
||||
color: this.props.fontColor,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
color: fontColor,
|
||||
};
|
||||
|
||||
return (
|
||||
@ -48,12 +57,15 @@ class ClosedCaptions extends Component {
|
||||
locale,
|
||||
captions,
|
||||
backgroundColor,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div disabled className={styles.ccbox}>
|
||||
<div className={styles.title}>
|
||||
<p> {locale} </p>
|
||||
<p>
|
||||
{ locale || intl.formatMessage(intlMessages.noLocaleSelected) }
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
ref={(ref) => { this.refCCScrollArea = ref; }}
|
||||
@ -69,7 +81,7 @@ class ClosedCaptions extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default injectWbResizeEvent(ClosedCaptions);
|
||||
export default injectIntl(injectWbResizeEvent(ClosedCaptions));
|
||||
|
||||
ClosedCaptions.propTypes = {
|
||||
backgroundColor: PropTypes.string.isRequired,
|
||||
@ -83,12 +95,15 @@ ClosedCaptions.propTypes = {
|
||||
).isRequired,
|
||||
}).isRequired,
|
||||
).isRequired,
|
||||
locale: PropTypes.string.isRequired,
|
||||
locale: PropTypes.string,
|
||||
fontColor: PropTypes.string.isRequired,
|
||||
fontSize: PropTypes.string.isRequired,
|
||||
fontFamily: PropTypes.string.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
ClosedCaptions.defaultProps = {
|
||||
locale: 'Locale is not selected',
|
||||
locale: undefined,
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import Settings from '/imports/ui/services/settings';
|
||||
import _ from 'lodash';
|
||||
|
||||
const getCCData = () => {
|
||||
const meetingID = Auth.meetingID;
|
||||
const { meetingID } = Auth;
|
||||
|
||||
const ccSettings = Settings.cc;
|
||||
|
||||
|
@ -53,7 +53,7 @@ const propTypes = {
|
||||
|
||||
const defaultProps = {
|
||||
children: null,
|
||||
isOpen: false,
|
||||
keepOpen: null,
|
||||
onShow: noop,
|
||||
onHide: noop,
|
||||
autoFocus: false,
|
||||
@ -78,10 +78,12 @@ class Dropdown extends Component {
|
||||
onShow,
|
||||
onHide,
|
||||
} = this.props;
|
||||
|
||||
const { isOpen } = this.state;
|
||||
|
||||
if (this.state.isOpen && !prevState.isOpen) { onShow(); }
|
||||
if (isOpen && !prevState.isOpen) { onShow(); }
|
||||
|
||||
if (!this.state.isOpen && prevState.isOpen) { onHide(); }
|
||||
if (!isOpen && prevState.isOpen) { onHide(); }
|
||||
}
|
||||
|
||||
handleShow() {
|
||||
@ -99,16 +101,29 @@ class Dropdown extends Component {
|
||||
}
|
||||
|
||||
handleWindowClick(event) {
|
||||
const { keepOpen, onHide } = this.props;
|
||||
const { isOpen } = this.state;
|
||||
const triggerElement = findDOMNode(this.trigger);
|
||||
const contentElement = findDOMNode(this.content);
|
||||
const closeDropdown = this.props.isOpen && this.state.isOpen && triggerElement.contains(event.target);
|
||||
const preventHide = this.props.isOpen && contentElement.contains(event.target) || !triggerElement;
|
||||
|
||||
if (closeDropdown) {
|
||||
return this.props.onHide();
|
||||
|
||||
if (keepOpen === null) {
|
||||
if (triggerElement.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentElement && preventHide) {
|
||||
if (triggerElement && triggerElement.contains(event.target)) {
|
||||
if (keepOpen) return onHide();
|
||||
if (isOpen) return this.handleHide();
|
||||
}
|
||||
|
||||
if (keepOpen && isOpen && !contentElement.contains(event.target)) {
|
||||
onHide();
|
||||
this.handleHide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (keepOpen !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -116,7 +131,8 @@ class Dropdown extends Component {
|
||||
}
|
||||
|
||||
handleToggle() {
|
||||
return this.state.isOpen ? this.handleHide() : this.handleShow();
|
||||
const { isOpen } = this.state;
|
||||
return isOpen ? this.handleHide() : this.handleShow();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -125,15 +141,18 @@ class Dropdown extends Component {
|
||||
className,
|
||||
style,
|
||||
intl,
|
||||
keepOpen,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const { isOpen } = this.state;
|
||||
|
||||
let trigger = children.find(x => x.type === DropdownTrigger);
|
||||
let content = children.find(x => x.type === DropdownContent);
|
||||
|
||||
trigger = React.cloneElement(trigger, {
|
||||
ref: (ref) => { this.trigger = ref; },
|
||||
dropdownIsOpen: this.state.isOpen,
|
||||
dropdownIsOpen: isOpen,
|
||||
dropdownToggle: this.handleToggle,
|
||||
dropdownShow: this.handleShow,
|
||||
dropdownHide: this.handleHide,
|
||||
@ -141,12 +160,14 @@ class Dropdown extends Component {
|
||||
|
||||
content = React.cloneElement(content, {
|
||||
ref: (ref) => { this.content = ref; },
|
||||
'aria-expanded': this.state.isOpen,
|
||||
dropdownIsOpen: this.state.isOpen,
|
||||
'aria-expanded': isOpen,
|
||||
dropdownIsOpen: isOpen,
|
||||
dropdownToggle: this.handleToggle,
|
||||
dropdownShow: this.handleShow,
|
||||
dropdownHide: this.handleHide,
|
||||
});
|
||||
|
||||
const showCloseBtn = (isOpen && keepOpen) || (isOpen && keepOpen === null);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -162,7 +183,7 @@ class Dropdown extends Component {
|
||||
>
|
||||
{trigger}
|
||||
{content}
|
||||
{this.state.isOpen ?
|
||||
{showCloseBtn ?
|
||||
<Button
|
||||
className={styles.close}
|
||||
label={intl.formatMessage(intlMessages.close)}
|
||||
|
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import Modal from '/imports/ui/components/modal/simple/component';
|
||||
import { styles } from './styles';
|
||||
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
endMeetingTitle: {
|
||||
id: 'app.endMeeting.title',
|
||||
description: 'end meeting title',
|
||||
},
|
||||
endMeetingDescription: {
|
||||
id: 'app.endMeeting.description',
|
||||
description: 'end meeting description',
|
||||
},
|
||||
yesLabel: {
|
||||
id: 'app.endMeeting.yesLabel',
|
||||
description: 'label for yes button for end meeting',
|
||||
},
|
||||
noLabel: {
|
||||
id: 'app.endMeeting.noLabel',
|
||||
description: 'label for no button for end meeting',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
endMeeting: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class EndMeetingComponent extends React.PureComponent {
|
||||
render() {
|
||||
const { intl, closeModal, endMeeting } = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
overlayClassName={styles.overlay}
|
||||
className={styles.modal}
|
||||
onRequestClose={closeModal}
|
||||
hideBorder
|
||||
title={intl.formatMessage(intlMessages.endMeetingTitle)}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.description}>
|
||||
{intl.formatMessage(intlMessages.endMeetingDescription)}
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
color="primary"
|
||||
className={styles.button}
|
||||
label={intl.formatMessage(intlMessages.yesLabel)}
|
||||
onClick={endMeeting}
|
||||
/>
|
||||
<Button
|
||||
label={intl.formatMessage(intlMessages.noLabel)}
|
||||
className={styles.button}
|
||||
onClick={closeModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EndMeetingComponent.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(EndMeetingComponent);
|
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import EndMeetingComponent from './component';
|
||||
|
||||
const EndMeetingContainer = props => <EndMeetingComponent {...props} />;
|
||||
|
||||
export default withModalMounter(withTracker(({ mountModal }) => ({
|
||||
closeModal() {
|
||||
mountModal(null);
|
||||
},
|
||||
|
||||
endMeeting: () => {
|
||||
makeCall('endMeeting');
|
||||
mountModal(null);
|
||||
},
|
||||
|
||||
}))(EndMeetingContainer));
|
@ -0,0 +1,58 @@
|
||||
@import '/imports/ui/stylesheets/mixins/focus';
|
||||
@import '/imports/ui/stylesheets/variables/_all';
|
||||
@import "/imports/ui/components/modal/simple/styles";
|
||||
|
||||
:root {
|
||||
--description-margin: 3.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--color-gray-dark);
|
||||
font-weight: var(--headings-font-weight);
|
||||
font-size: var(--jumbo-padding-y);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
margin-right: var(--description-margin);
|
||||
margin-left: var(--description-margin);
|
||||
margin-bottom: var(--lg-padding-x);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display:flex;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding-right: var(--jumbo-padding-y);
|
||||
padding-left: var(--jumbo-padding-y);
|
||||
margin-right: var(--sm-padding-x);
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-gray);
|
||||
margin-bottom: var(--jumbo-padding-y)
|
||||
}
|
||||
|
||||
.modal {
|
||||
@extend .modal;
|
||||
padding: var(--sm-padding-y);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@extend .overlay;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
line-height: var(--title-position-left);
|
||||
margin-bottom: var(--lg-padding-y);
|
||||
}
|
@ -4,6 +4,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import logoutRouteHandler from '/imports/utils/logoutRouteHandler';
|
||||
import { Session } from 'meteor/session';
|
||||
import { styles } from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -18,6 +19,9 @@ const intlMessages = defineMessages({
|
||||
401: {
|
||||
id: 'app.error.401',
|
||||
},
|
||||
400: {
|
||||
id: 'app.error.400',
|
||||
},
|
||||
leave: {
|
||||
id: 'app.error.leaveLabel',
|
||||
description: 'aria-label for leaving',
|
||||
@ -55,18 +59,27 @@ class ErrorScreen extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<div className={styles.background}>
|
||||
<h1>
|
||||
<h1 className={styles.codeError}>
|
||||
{code}
|
||||
</h1>
|
||||
<h1 className={styles.message}>
|
||||
{formatedMessage}
|
||||
</h1>
|
||||
<div className={styles.separator} />
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
{
|
||||
!Session.get('errorMessageDescription') || (
|
||||
<div className={styles.sessionMessage}>
|
||||
{Session.get('errorMessageDescription')}
|
||||
</div>)
|
||||
}
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
className={styles.button}
|
||||
onClick={logoutRouteHandler}
|
||||
label={intl.formatMessage(intlMessages.leave)}
|
||||
/>
|
||||
|
@ -19,7 +19,35 @@
|
||||
}
|
||||
|
||||
.message {
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
text-transform: capitalize;
|
||||
color: var(--color-gray-light);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.sessionMessage {
|
||||
@extend .message;
|
||||
font-size: var(--font-size-small);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.codeError {
|
||||
margin: 0;
|
||||
font-size: 5rem;
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 0;
|
||||
width: 5rem;
|
||||
border: 1px solid var(--color-gray-lighter);
|
||||
margin: 1.5rem 0 1.5rem 0;
|
||||
align-self: center;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 9rem;
|
||||
height: 2rem;
|
||||
}
|
@ -0,0 +1,211 @@
|
||||
import React, { Component } from 'react';
|
||||
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
||||
import YouTube from 'react-youtube';
|
||||
import Vimeo from 'react-vimeo';
|
||||
import { sendMessage, onMessage } from './service';
|
||||
|
||||
const { PlayerState } = YouTube;
|
||||
|
||||
class VideoPlayer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.player = null;
|
||||
this.syncInterval = null;
|
||||
this.playerState = PlayerState.UNSTARTED;
|
||||
this.presenterCommand = false;
|
||||
this.preventStateChange = false;
|
||||
this.opts = {
|
||||
playerVars: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
autoplay: 1,
|
||||
modestbranding: true,
|
||||
rel: 0,
|
||||
ecver: 2,
|
||||
},
|
||||
};
|
||||
|
||||
this.keepSync = this.keepSync.bind(this);
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
this.handleOnReady = this.handleOnReady.bind(this);
|
||||
this.handleStateChange = this.handleStateChange.bind(this);
|
||||
this.resizeListener = () => {
|
||||
setTimeout(this.handleResize, 0);
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.resizeListener);
|
||||
}
|
||||
|
||||
componentDidUpdate(nextProps) {
|
||||
if (!nextProps.videoId) {
|
||||
clearInterval(this.syncInterval);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.resizeListener);
|
||||
|
||||
clearInterval(this.syncInterval);
|
||||
this.player = null;
|
||||
this.refPlayer = null;
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
if (!this.player || !this.refPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = this.refPlayer;
|
||||
const parent = el.parentElement;
|
||||
const w = parent.clientWidth;
|
||||
const h = parent.clientHeight;
|
||||
const idealW = h * 16 / 9;
|
||||
|
||||
if (idealW > w) {
|
||||
this.player.setSize(w, w * 9 / 16);
|
||||
} else {
|
||||
this.player.setSize(idealW, h);
|
||||
}
|
||||
}
|
||||
|
||||
keepSync() {
|
||||
const { isPresenter } = this.props;
|
||||
|
||||
if (isPresenter) {
|
||||
this.syncInterval = setInterval(() => {
|
||||
const curTime = this.player.getCurrentTime();
|
||||
const rate = this.player.getPlaybackRate();
|
||||
sendMessage('playerUpdate', { rate, time: curTime, state: this.playerState });
|
||||
}, 2000);
|
||||
} else {
|
||||
onMessage('play', ({ time }) => {
|
||||
this.presenterCommand = true;
|
||||
if (this.player) {
|
||||
this.player.seekTo(time, true);
|
||||
this.playerState = PlayerState.PLAYING;
|
||||
this.player.playVideo();
|
||||
}
|
||||
});
|
||||
|
||||
onMessage('stop', ({ time }) => {
|
||||
this.presenterCommand = true;
|
||||
|
||||
if (this.player) {
|
||||
this.playerState = PlayerState.PAUSED;
|
||||
this.player.seekTo(time, true);
|
||||
this.player.pauseVideo();
|
||||
}
|
||||
});
|
||||
|
||||
onMessage('playerUpdate', (data) => {
|
||||
if (!this.player) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.rate !== this.player.getPlaybackRate()) {
|
||||
this.player.setPlaybackRate(data.rate);
|
||||
}
|
||||
|
||||
if (Math.abs(this.player.getCurrentTime() - data.time) > 2) {
|
||||
this.player.seekTo(data.time, true);
|
||||
}
|
||||
|
||||
if (this.playerState !== data.state) {
|
||||
this.presenterCommand = true;
|
||||
this.playerState = data.state;
|
||||
if (this.playerState === PlayerState.PLAYING) {
|
||||
this.player.playVideo();
|
||||
} else {
|
||||
this.player.pauseVideo();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMessage('changePlaybackRate', (rate) => {
|
||||
this.player.setPlaybackRate(rate);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleOnReady(event) {
|
||||
const { isPresenter } = this.props;
|
||||
|
||||
this.player = event.target;
|
||||
this.player.pauseVideo();
|
||||
|
||||
this.keepSync();
|
||||
|
||||
if (!isPresenter) {
|
||||
sendMessage('viewerJoined');
|
||||
} else {
|
||||
this.player.playVideo();
|
||||
}
|
||||
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
handleStateChange(event) {
|
||||
const { isPresenter } = this.props;
|
||||
const curTime = this.player.getCurrentTime();
|
||||
|
||||
if (this.preventStateChange && [PlayerState.PLAYING, PlayerState.PAUSED].includes(event.data)) {
|
||||
this.preventStateChange = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.playerState === event.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data === PlayerState.PLAYING) {
|
||||
if (isPresenter) {
|
||||
sendMessage('play', { time: curTime });
|
||||
this.playerState = event.data;
|
||||
} else if (!this.presenterCommand) {
|
||||
this.player.seekTo(curTime, true);
|
||||
this.preventStateChange = true;
|
||||
this.player.pauseVideo();
|
||||
} else {
|
||||
this.playerState = event.data;
|
||||
this.presenterCommand = false;
|
||||
}
|
||||
} else if (event.data === PlayerState.PAUSED) {
|
||||
if (isPresenter) {
|
||||
sendMessage('stop', { time: curTime });
|
||||
this.playerState = event.data;
|
||||
} else if (!this.presenterCommand) {
|
||||
this.player.seekTo(curTime);
|
||||
this.preventStateChange = true;
|
||||
this.player.playVideo();
|
||||
} else {
|
||||
this.playerState = event.data;
|
||||
this.presenterCommand = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { videoId } = this.props;
|
||||
const { opts, handleOnReady, handleStateChange } = this;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="youtube-video-player"
|
||||
data-test="videoPlayer"
|
||||
ref={(ref) => { this.refPlayer = ref; }}
|
||||
>
|
||||
<YouTube
|
||||
videoId={videoId}
|
||||
opts={opts}
|
||||
onReady={handleOnReady}
|
||||
onStateChange={handleStateChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectWbResizeEvent(VideoPlayer);
|
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import ExternalVideo from './component';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
title: {
|
||||
id: 'app.externalVideo.title',
|
||||
description: '',
|
||||
},
|
||||
});
|
||||
|
||||
const ExternalVideoContainer = props => (
|
||||
<ExternalVideo {...props}>
|
||||
{props.children}
|
||||
</ExternalVideo>
|
||||
);
|
||||
|
||||
export default injectIntl(withTracker(({ params, intl }) => {
|
||||
const title = intl.formatMessage(intlMessages.title);
|
||||
|
||||
return {
|
||||
title,
|
||||
};
|
||||
})(ExternalVideoContainer));
|
@ -0,0 +1,154 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
|
||||
import ModalBase from '/imports/ui/components/modal/base/component';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { isUrlValid, getUrlFromVideoId } from '../service';
|
||||
|
||||
import { styles } from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
start: {
|
||||
id: 'app.externalVideo.start',
|
||||
description: 'Share youtube video',
|
||||
},
|
||||
stop: {
|
||||
id: 'app.externalVideo.stop',
|
||||
description: 'Stop sharing video',
|
||||
},
|
||||
urlError: {
|
||||
id: 'app.externalVideo.urlError',
|
||||
description: 'Not a video URL error',
|
||||
},
|
||||
input: {
|
||||
id: 'app.externalVideo.input',
|
||||
description: 'Video URL',
|
||||
},
|
||||
title: {
|
||||
id: 'app.externalVideo.title',
|
||||
description: 'Modal title',
|
||||
},
|
||||
close: {
|
||||
id: 'app.externalVideo.close',
|
||||
description: 'Close',
|
||||
},
|
||||
});
|
||||
|
||||
class ExternalVideoModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { videoId } = props;
|
||||
|
||||
this.state = {
|
||||
url: getUrlFromVideoId(videoId),
|
||||
sharing: videoId,
|
||||
};
|
||||
|
||||
this.startWatchingHandler = this.startWatchingHandler.bind(this);
|
||||
this.stopWatchingHandler = this.stopWatchingHandler.bind(this);
|
||||
this.updateVideoUrlHandler = this.updateVideoUrlHandler.bind(this);
|
||||
this.renderUrlError = this.renderUrlError.bind(this);
|
||||
this.updateVideoUrlHandler = this.updateVideoUrlHandler.bind(this);
|
||||
}
|
||||
|
||||
startWatchingHandler() {
|
||||
const { startWatching, closeModal } = this.props;
|
||||
const { url } = this.state;
|
||||
|
||||
startWatching(url);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
stopWatchingHandler() {
|
||||
const { stopWatching, closeModal } = this.props;
|
||||
|
||||
stopWatching();
|
||||
closeModal();
|
||||
}
|
||||
|
||||
updateVideoUrlHandler(ev) {
|
||||
this.setState({ url: ev.target.value });
|
||||
}
|
||||
|
||||
renderUrlError() {
|
||||
const { intl } = this.props;
|
||||
const { url } = this.state;
|
||||
|
||||
const valid = (!url || url.length <= 3) || isUrlValid(url);
|
||||
|
||||
return (
|
||||
!valid
|
||||
? (
|
||||
<div className={styles.urlError}>
|
||||
{intl.formatMessage(intlMessages.urlError)}
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, videoId, closeModal } = this.props;
|
||||
const { url, sharing } = this.state;
|
||||
|
||||
const startDisabled = !isUrlValid(url) || (getUrlFromVideoId(videoId) === url);
|
||||
|
||||
return (
|
||||
<ModalBase
|
||||
overlayClassName={styles.overlay}
|
||||
className={styles.modal}
|
||||
onRequestClose={closeModal}
|
||||
>
|
||||
<header data-test="videoModealHeader" className={styles.header}>
|
||||
<h3 className={styles.title}>{intl.formatMessage(intlMessages.title)}</h3>
|
||||
<Button
|
||||
data-test="modalBaseCloseButton"
|
||||
className={styles.closeBtn}
|
||||
label={intl.formatMessage(intlMessages.close)}
|
||||
icon="close"
|
||||
size="md"
|
||||
hideLabel
|
||||
onClick={closeModal}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.videoUrl}>
|
||||
<label htmlFor="video-modal-input" id="video-modal-input">
|
||||
{intl.formatMessage(intlMessages.input)}
|
||||
<input
|
||||
id="video-modal-input"
|
||||
onChange={this.updateVideoUrlHandler}
|
||||
name="video-modal-input"
|
||||
value={url}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
{this.renderUrlError()}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className={styles.startBtn}
|
||||
label={intl.formatMessage(intlMessages.start)}
|
||||
onClick={this.startWatchingHandler}
|
||||
disabled={startDisabled}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className={styles.stopBtn}
|
||||
label={intl.formatMessage(intlMessages.stop)}
|
||||
onClick={this.stopWatchingHandler}
|
||||
disabled={!sharing}
|
||||
/>
|
||||
</div>
|
||||
</ModalBase>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(withModalMounter(ExternalVideoModal));
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
import ExternalVideoModal from './component';
|
||||
import { startWatching, stopWatching, getVideoId } from '../service';
|
||||
|
||||
const ExternalVideoModalContainer = props => <ExternalVideoModal {...props} />;
|
||||
|
||||
export default withModalMounter(withTracker(({ mountModal }) => ({
|
||||
closeModal: () => {
|
||||
mountModal(null);
|
||||
},
|
||||
startWatching,
|
||||
stopWatching,
|
||||
videoId: getVideoId(),
|
||||
}))(ExternalVideoModalContainer));
|
@ -0,0 +1,162 @@
|
||||
@import "/imports/ui/stylesheets/variables/_all";
|
||||
@import "/imports/ui/components/modal/simple/styles";
|
||||
|
||||
.header {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
justify-content: center;
|
||||
padding-bottom: 0;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.videoOptions {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@extend .overlay;
|
||||
}
|
||||
|
||||
.modal {
|
||||
@extend .modal;
|
||||
padding: 1.5rem;
|
||||
min-height: 20rem;
|
||||
}
|
||||
|
||||
.closeBtn {
|
||||
position: relative;
|
||||
background-color: var(--color-white);
|
||||
|
||||
i {
|
||||
color: var(--color-gray-light);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover{
|
||||
background-color: var(--color-gray-lighter);
|
||||
i{
|
||||
color: var(--color-gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
.warning{
|
||||
text-align: center;
|
||||
font-weight: var(--headings-font-weight);
|
||||
font-size: 5rem;
|
||||
white-space: normal;
|
||||
}
|
||||
.text{
|
||||
margin: var(--line-height-computed);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
margin: var(--line-height-computed);
|
||||
text-align: center;
|
||||
font-size: var(--font-size-large);
|
||||
}
|
||||
|
||||
.startBtn {
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
i{
|
||||
color: #3c5764;
|
||||
}
|
||||
|
||||
margin: 0;
|
||||
width: 40%;
|
||||
|
||||
display: block;
|
||||
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
|
||||
color: var(--color-white) !important;
|
||||
background-color: var(--color-link) !important;
|
||||
}
|
||||
|
||||
.stopBtn {
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
i{
|
||||
color: red;
|
||||
}
|
||||
|
||||
margin-left: 50%;
|
||||
width: 40%;
|
||||
|
||||
display: block;
|
||||
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
|
||||
color: var(--color-white) !important;
|
||||
background-color: red !important;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 1.3rem;
|
||||
white-space: normal;
|
||||
|
||||
@include mq(var(--small-only)) {
|
||||
font-size: 1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.videoUrl {
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
margin: 10px 0 10px 0;
|
||||
|
||||
padding: 0.4em;
|
||||
|
||||
background-color: #F1F8FF;
|
||||
|
||||
line-height: 2rem;
|
||||
|
||||
width: 100%;
|
||||
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
border: none;
|
||||
|
||||
border-radius: 0.4rem;
|
||||
transition: box-shadow var(--transitionDuration);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0.2rem 0.8rem 1.6rem 600;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.urlError {
|
||||
color: red;
|
||||
padding: 1em;
|
||||
transition: 1s;
|
||||
}
|
||||
|
@ -0,0 +1,61 @@
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import ExternalVideoStreamer from '/imports/api/external-videos';
|
||||
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
|
||||
const YOUTUBE_PREFIX = 'https://youtube.com/watch?v=';
|
||||
|
||||
const isUrlEmpty = url => !url || url.length === 0;
|
||||
|
||||
const isUrlValid = (url) => {
|
||||
const regexp = RegExp('^(https?://)?(www.)?(youtube.com|youtu.?be)/.+$');
|
||||
return !isUrlEmpty(url) && url.match(regexp);
|
||||
};
|
||||
|
||||
const getUrlFromVideoId = id => (id ? `${YOUTUBE_PREFIX}${id}` : '');
|
||||
|
||||
const videoIdFromUrl = (url) => {
|
||||
const urlObj = new URL(url);
|
||||
const params = new URLSearchParams(urlObj.search);
|
||||
|
||||
return params.get('v');
|
||||
};
|
||||
|
||||
const startWatching = (url) => {
|
||||
const externalVideoUrl = videoIdFromUrl(url);
|
||||
makeCall('startWatchingExternalVideo', { externalVideoUrl });
|
||||
};
|
||||
|
||||
const stopWatching = () => {
|
||||
makeCall('stopWatchingExternalVideo');
|
||||
};
|
||||
|
||||
const sendMessage = (event, data) => {
|
||||
ExternalVideoStreamer.emit(event, {
|
||||
...data,
|
||||
meetingId: Auth.meetingID,
|
||||
userId: Auth.userID,
|
||||
});
|
||||
};
|
||||
|
||||
const onMessage = (message, func) => {
|
||||
ExternalVideoStreamer.on(message, func);
|
||||
};
|
||||
|
||||
const getVideoId = () => {
|
||||
const meetingId = Auth.meetingID;
|
||||
const meeting = Meetings.findOne({ meetingId });
|
||||
|
||||
return meeting && meeting.externalVideoUrl;
|
||||
};
|
||||
|
||||
export {
|
||||
sendMessage,
|
||||
onMessage,
|
||||
getVideoId,
|
||||
getUrlFromVideoId,
|
||||
isUrlValid,
|
||||
startWatching,
|
||||
stopWatching,
|
||||
};
|
@ -0,0 +1,52 @@
|
||||
@import "/imports/ui/stylesheets/mixins/focus";
|
||||
@import "/imports/ui/stylesheets/variables/_all";
|
||||
|
||||
$icon-offset: -.4em;
|
||||
|
||||
.note {
|
||||
background-color: #fff;
|
||||
padding-top: var(--md-padding-x);
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: left;
|
||||
flex-shrink: 0;
|
||||
padding-left: var(--md-padding-x);
|
||||
padding-right: var(--md-padding-x);
|
||||
|
||||
a {
|
||||
@include elementFocus(var(--color-primary));
|
||||
padding-bottom: var(--sm-padding-y);
|
||||
padding-left: var(--sm-padding-y);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
[class^="icon-bbb-"],
|
||||
[class*=" icon-bbb-"] {
|
||||
font-size: 85%;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@extend %text-elipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
iframe {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
border-style: none;
|
||||
}
|
@ -13,6 +13,7 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
const { showParticipantsOnLogin } = APP_CONFIG;
|
||||
|
||||
class JoinHandler extends Component {
|
||||
static setError(codeError) {
|
||||
@ -41,8 +42,10 @@ class JoinHandler extends Component {
|
||||
async fetchToken() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const sessionToken = urlParams.get('sessionToken');
|
||||
|
||||
if (!sessionToken) {
|
||||
JoinHandler.setError('404');
|
||||
JoinHandler.setError('400');
|
||||
Session.set('errorMessageDescription', 'Session token was not provided');
|
||||
}
|
||||
|
||||
// Old credentials stored in memory were being used when joining a new meeting
|
||||
@ -108,27 +111,26 @@ class JoinHandler extends Component {
|
||||
const fetchContent = await fetch(url, { credentials: 'same-origin' });
|
||||
const parseToJson = await fetchContent.json();
|
||||
const { response } = parseToJson;
|
||||
|
||||
setLogoutURL(response);
|
||||
|
||||
if (response.returncode !== 'FAILED') {
|
||||
await setAuth(response);
|
||||
await setCustomData(response);
|
||||
setLogoURL(response);
|
||||
logUserInfo();
|
||||
|
||||
const { showParticipantsOnLogin } = APP_CONFIG;
|
||||
|
||||
if (showParticipantsOnLogin) {
|
||||
if (showParticipantsOnLogin && !deviceInfo.type().isPhone) {
|
||||
Session.set('openPanel', 'chat');
|
||||
Session.set('idChatOpen', '');
|
||||
if (deviceInfo.type().isPhone) Session.set('openPanel', '');
|
||||
} else {
|
||||
Session.set('openPanel', '');
|
||||
}
|
||||
|
||||
|
||||
logger.info(`User successfully went through main.joinRouteHandler with [${JSON.stringify(response)}].`);
|
||||
} else {
|
||||
const e = new Error('Session not found');
|
||||
const e = new Error(response.message);
|
||||
if (!Session.get('codeError')) Session.set('errorMessageDescription', response.message);
|
||||
logger.error(`User faced [${e}] on main.joinRouteHandler. Error was:`, JSON.stringify(response));
|
||||
}
|
||||
this.changeToJoin(true);
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import Toggle from '/imports/ui/components/switch/component';
|
||||
import cx from 'classnames';
|
||||
import ModalBase from '/imports/ui/components/modal/base/component';
|
||||
import Modal from '/imports/ui/components/modal/simple/component';
|
||||
import { styles } from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -51,48 +50,29 @@ const intlMessages = defineMessages({
|
||||
id: 'app.lock-viewers.PrivateChatLable',
|
||||
description: 'description for close button',
|
||||
},
|
||||
layoutLable: {
|
||||
id: 'app.lock-viewers.Layout',
|
||||
description: 'description for close button',
|
||||
},
|
||||
});
|
||||
|
||||
class LockViewersComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
class LockViewersComponent extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
meeting,
|
||||
closeModal,
|
||||
toggleLockSettings,
|
||||
toggleWebcamsOnlyForModerator,
|
||||
} = props;
|
||||
|
||||
this.closeModal = closeModal;
|
||||
this.toggleLockSettings = toggleLockSettings;
|
||||
this.toggleWebcamsOnlyForModerator = toggleWebcamsOnlyForModerator;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, meeting } = this.props;
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalBase
|
||||
<Modal
|
||||
overlayClassName={styles.overlay}
|
||||
className={styles.modal}
|
||||
onRequestClose={this.closeModal}
|
||||
onRequestClose={closeModal}
|
||||
hideBorder
|
||||
>
|
||||
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>{intl.formatMessage(intlMessages.lockViewersTitle)}</div>
|
||||
<Button
|
||||
data-test="modalBaseCloseButton"
|
||||
className={styles.closeBtn}
|
||||
label={intl.formatMessage(intlMessages.closeLabel)}
|
||||
icon="close"
|
||||
size="md"
|
||||
hideLabel
|
||||
onClick={this.closeModal}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
{`${intl.formatMessage(intlMessages.lockViewersDescription)}`}
|
||||
@ -117,9 +97,8 @@ class LockViewersComponent extends Component {
|
||||
icons={false}
|
||||
defaultChecked={meeting.lockSettingsProp.disableCam}
|
||||
onChange={() => {
|
||||
meeting.lockSettingsProp.disableCam =
|
||||
!meeting.lockSettingsProp.disableCam;
|
||||
this.toggleLockSettings(meeting);
|
||||
meeting.lockSettingsProp.disableCam = !meeting.lockSettingsProp.disableCam;
|
||||
toggleLockSettings(meeting);
|
||||
}}
|
||||
ariaLabel={intl.formatMessage(intlMessages.webcamLabel)}
|
||||
/>
|
||||
@ -140,9 +119,8 @@ class LockViewersComponent extends Component {
|
||||
icons={false}
|
||||
defaultChecked={meeting.usersProp.webcamsOnlyForModerator}
|
||||
onChange={() => {
|
||||
meeting.usersProp.webcamsOnlyForModerator =
|
||||
!meeting.usersProp.webcamsOnlyForModerator;
|
||||
this.toggleWebcamsOnlyForModerator(meeting);
|
||||
meeting.usersProp.webcamsOnlyForModerator = !meeting.usersProp.webcamsOnlyForModerator;
|
||||
toggleWebcamsOnlyForModerator(meeting);
|
||||
}}
|
||||
ariaLabel={intl.formatMessage(intlMessages.otherViewersWebcamLabel)}
|
||||
/>
|
||||
@ -163,9 +141,8 @@ class LockViewersComponent extends Component {
|
||||
icons={false}
|
||||
defaultChecked={meeting.lockSettingsProp.disableMic}
|
||||
onChange={() => {
|
||||
meeting.lockSettingsProp.disableMic =
|
||||
!meeting.lockSettingsProp.disableMic;
|
||||
this.toggleLockSettings(meeting);
|
||||
meeting.lockSettingsProp.disableMic = !meeting.lockSettingsProp.disableMic;
|
||||
toggleLockSettings(meeting);
|
||||
}}
|
||||
ariaLabel={intl.formatMessage(intlMessages.microphoneLable)}
|
||||
/>
|
||||
@ -186,9 +163,8 @@ class LockViewersComponent extends Component {
|
||||
icons={false}
|
||||
defaultChecked={meeting.lockSettingsProp.disablePubChat}
|
||||
onChange={() => {
|
||||
meeting.lockSettingsProp.disablePubChat =
|
||||
!meeting.lockSettingsProp.disablePubChat;
|
||||
this.toggleLockSettings(meeting);
|
||||
meeting.lockSettingsProp.disablePubChat = !meeting.lockSettingsProp.disablePubChat;
|
||||
toggleLockSettings(meeting);
|
||||
}}
|
||||
ariaLabel={intl.formatMessage(intlMessages.publicChatLabel)}
|
||||
/>
|
||||
@ -209,41 +185,17 @@ class LockViewersComponent extends Component {
|
||||
icons={false}
|
||||
defaultChecked={meeting.lockSettingsProp.disablePrivChat}
|
||||
onChange={() => {
|
||||
meeting.lockSettingsProp.disablePrivChat =
|
||||
!meeting.lockSettingsProp.disablePrivChat;
|
||||
this.toggleLockSettings(meeting);
|
||||
meeting.lockSettingsProp.disablePrivChat = !meeting.lockSettingsProp.disablePrivChat;
|
||||
toggleLockSettings(meeting);
|
||||
}}
|
||||
ariaLabel={intl.formatMessage(intlMessages.privateChatLable)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.col} aria-hidden="true">
|
||||
<div className={styles.formElement}>
|
||||
<div className={styles.label}>
|
||||
{intl.formatMessage(intlMessages.layoutLable)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.col}>
|
||||
<div className={cx(styles.formElement, styles.pullContentRight)}>
|
||||
<Toggle
|
||||
icons={false}
|
||||
defaultChecked={meeting.lockSettingsProp.lockedLayout}
|
||||
onChange={() => {
|
||||
meeting.lockSettingsProp.lockedLayout =
|
||||
!meeting.lockSettingsProp.lockedLayout;
|
||||
this.toggleLockSettings(meeting);
|
||||
}}
|
||||
ariaLabel={intl.formatMessage(intlMessages.layoutLable)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBase>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
left: var(--title-position-left);
|
||||
color: var(--color-gray-dark);
|
||||
font-weight: bold;
|
||||
@ -24,6 +23,7 @@
|
||||
|
||||
.container {
|
||||
margin: var(--modal-margin);
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--lg-padding-x);
|
||||
}
|
||||
|
||||
|
@ -1,91 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import Modal from '/imports/ui/components/modal/fullscreen/component';
|
||||
import { styles } from './styles';
|
||||
|
||||
const propTypes = {
|
||||
handleEndMeeting: PropTypes.func.isRequired,
|
||||
confirmLeaving: PropTypes.func.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
showEndMeeting: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
title: {
|
||||
id: 'app.leaveConfirmation.title',
|
||||
description: 'Leave session modal title',
|
||||
},
|
||||
message: {
|
||||
id: 'app.leaveConfirmation.message',
|
||||
description: 'message for leaving session',
|
||||
},
|
||||
confirmLabel: {
|
||||
id: 'app.leaveConfirmation.confirmLabel',
|
||||
description: 'Confirmation button label',
|
||||
},
|
||||
confirmDesc: {
|
||||
id: 'app.leaveConfirmation.confirmDesc',
|
||||
description: 'adds context to confim option',
|
||||
},
|
||||
dismissLabel: {
|
||||
id: 'app.leaveConfirmation.dismissLabel',
|
||||
description: 'Dismiss button label',
|
||||
},
|
||||
dismissDesc: {
|
||||
id: 'app.leaveConfirmation.dismissDesc',
|
||||
description: 'adds context to dismiss option',
|
||||
},
|
||||
endMeetingLabel: {
|
||||
id: 'app.leaveConfirmation.endMeetingLabel',
|
||||
description: 'End meeting button label',
|
||||
},
|
||||
endMeetingAriaLabel: {
|
||||
id: 'app.leaveConfirmation.endMeetingAriaLabel',
|
||||
description: 'End meeting button aria label',
|
||||
},
|
||||
endMeetingDesc: {
|
||||
id: 'app.leaveConfirmation.endMeetingDesc',
|
||||
description: 'adds context to end meeting option',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const LeaveConfirmation = ({
|
||||
intl,
|
||||
handleEndMeeting,
|
||||
showEndMeeting,
|
||||
confirmLeaving,
|
||||
}) => (
|
||||
<Modal
|
||||
title={intl.formatMessage(intlMessages.title)}
|
||||
confirm={{
|
||||
callback: confirmLeaving,
|
||||
label: intl.formatMessage(intlMessages.confirmLabel),
|
||||
description: intl.formatMessage(intlMessages.confirmDesc),
|
||||
}}
|
||||
dismiss={{
|
||||
callback: () => null,
|
||||
label: intl.formatMessage(intlMessages.dismissLabel),
|
||||
description: intl.formatMessage(intlMessages.dismissDesc),
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.message)}
|
||||
{showEndMeeting ?
|
||||
<Button
|
||||
className={styles.endMeeting}
|
||||
label={intl.formatMessage(intlMessages.endMeetingLabel)}
|
||||
onClick={handleEndMeeting}
|
||||
aria-describedby="modalEndMeetingDesc"
|
||||
/> : null
|
||||
}
|
||||
<div id="modalEndMeetingDesc" hidden>{intl.formatMessage(intlMessages.endMeetingDesc)}</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
LeaveConfirmation.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(LeaveConfirmation);
|
@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import { meetingIsBreakout } from '/imports/ui/components/app/service';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { Session } from 'meteor/session';
|
||||
|
||||
import LogoutConfirmation from './component';
|
||||
import {
|
||||
isModerator,
|
||||
endMeeting,
|
||||
} from './service';
|
||||
|
||||
const LogoutConfirmationContainer = props => (
|
||||
<LogoutConfirmation {...props} />
|
||||
);
|
||||
|
||||
export default withTracker(() => {
|
||||
const confirmLeaving = () => {
|
||||
Session.set('isMeetingEnded', true);
|
||||
Session.set('codeError', '430');
|
||||
};
|
||||
|
||||
return {
|
||||
showEndMeeting: !meetingIsBreakout() && isModerator(),
|
||||
handleEndMeeting: endMeeting,
|
||||
confirmLeaving,
|
||||
};
|
||||
})(LogoutConfirmationContainer);
|
@ -1,20 +0,0 @@
|
||||
import { makeCall } from '/imports/ui/services/api/index';
|
||||
import Users from '/imports/api/users';
|
||||
import mapUser from '/imports/ui/services/user/mapUser';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
|
||||
const endMeeting = () => {
|
||||
makeCall('endMeeting', Auth.credentials);
|
||||
};
|
||||
|
||||
const isModerator = () => {
|
||||
const currentUserId = Auth.userID;
|
||||
const currentUser = Users.findOne({ userId: currentUserId });
|
||||
|
||||
return (currentUser) ? mapUser(currentUser).isModerator : null;
|
||||
};
|
||||
|
||||
export {
|
||||
endMeeting,
|
||||
isModerator,
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
%btn {
|
||||
flex: 0 1 48%;
|
||||
}
|
||||
|
||||
.endMeeting {
|
||||
@extend %btn;
|
||||
margin-left: 5rem;
|
||||
}
|
@ -7,12 +7,13 @@ import { notify } from '/imports/ui/services/notification';
|
||||
import VideoService from '/imports/ui/components/video-provider/service';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
|
||||
import Media from './component';
|
||||
import MediaService, { getSwapLayout } from './service';
|
||||
import PresentationPodsContainer from '../presentation-pod/container';
|
||||
import ScreenshareContainer from '../screenshare/container';
|
||||
import DefaultContent from '../presentation/default-content/component';
|
||||
import ExternalVideoContainer from '../external-video-player/container';
|
||||
import { getVideoId } from '../external-video-player/service';
|
||||
|
||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||
const KURENTO_CONFIG = Meteor.settings.public.kurento;
|
||||
@ -80,8 +81,9 @@ class MediaContainer extends Component {
|
||||
|
||||
const chromeErrorElement = (
|
||||
<div>
|
||||
{intl.formatMessage(intlMessages.chromeExtensionError)}{' '}
|
||||
<a href={CHROME_EXTENSION_LINK} target="_blank">
|
||||
{intl.formatMessage(intlMessages.chromeExtensionError)}
|
||||
{' '}
|
||||
<a href={CHROME_EXTENSION_LINK} target="_blank" rel="noopener noreferrer">
|
||||
{intl.formatMessage(intlMessages.chromeExtensionErrorLink)}
|
||||
</a>
|
||||
</div>
|
||||
@ -99,7 +101,7 @@ class MediaContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default withModalMounter(withTracker(({ mountModal }) => {
|
||||
export default withModalMounter(withTracker(() => {
|
||||
const { dataSaving } = Settings;
|
||||
const { viewParticipantsWebcams, viewScreenshare } = dataSaving;
|
||||
|
||||
@ -118,7 +120,7 @@ export default withModalMounter(withTracker(({ mountModal }) => {
|
||||
}
|
||||
|
||||
const usersVideo = VideoService.getAllUsersVideo();
|
||||
if (MediaService.shouldShowOverlay() && usersVideo.length) {
|
||||
if (MediaService.shouldShowOverlay() && usersVideo.length && viewParticipantsWebcams) {
|
||||
data.floatingOverlay = usersVideo.length < 2;
|
||||
data.hideOverlay = usersVideo.length === 0;
|
||||
}
|
||||
@ -129,7 +131,20 @@ export default withModalMounter(withTracker(({ mountModal }) => {
|
||||
|
||||
if (data.swapLayout) {
|
||||
data.floatingOverlay = true;
|
||||
data.hideOverlay = hidePresentation;
|
||||
data.hideOverlay = true;
|
||||
}
|
||||
|
||||
if (data.isScreensharing) {
|
||||
data.floatingOverlay = false;
|
||||
}
|
||||
|
||||
if (MediaService.shouldShowExternalVideo()) {
|
||||
data.children = (
|
||||
<ExternalVideoContainer
|
||||
isPresenter={MediaService.isUserPresenter()}
|
||||
videoId={getVideoId()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
MediaContainer.propTypes = propTypes;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Presentations from '/imports/api/presentations';
|
||||
import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service';
|
||||
import { getVideoId } from '/imports/ui/components/external-video-player/service';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Users from '/imports/api/users';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
@ -30,6 +31,10 @@ function shouldShowScreenshare() {
|
||||
return isVideoBroadcasting() && getFromUserSettings('enableScreensharing', KURENTO_CONFIG.enableScreensharing);
|
||||
}
|
||||
|
||||
function shouldShowExternalVideo() {
|
||||
return getVideoId() && Meteor.settings.public.app.enableExternalVideo;
|
||||
}
|
||||
|
||||
function shouldShowOverlay() {
|
||||
return getFromUserSettings('enableVideo', KURENTO_CONFIG.enableVideo);
|
||||
}
|
||||
@ -64,6 +69,7 @@ export default {
|
||||
getPresentationInfo,
|
||||
shouldShowWhiteboard,
|
||||
shouldShowScreenshare,
|
||||
shouldShowExternalVideo,
|
||||
shouldShowOverlay,
|
||||
isUserPresenter,
|
||||
isVideoBroadcasting,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user