Merge branch 'develop' into analytics_actor_for_meeting_info
This commit is contained in:
commit
7c06f3b85e
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -20,9 +20,11 @@ HOW TO WRITE A GOOD PULL REQUEST?
|
||||
<!-- A brief description of each change being made with this pull request. -->
|
||||
|
||||
### Closes Issue(s)
|
||||
<!-- List here all the issues closed by this pull request. Use keyword `closes` before each issue number
|
||||
Closes #123456
|
||||
-->
|
||||
Closes #
|
||||
|
||||
closes #...
|
||||
<!-- List here all the issues closed by this pull request. Use keyword `closes` before each issue number -->
|
||||
|
||||
### Motivation
|
||||
|
||||
|
@ -31,8 +31,8 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
|
||||
userValidated(u, state)
|
||||
} else {
|
||||
if (u.banned) {
|
||||
failReason = "Ejected user rejoining"
|
||||
failReasonCode = EjectReasonCode.EJECTED_USER_REJOINING
|
||||
failReason = "Banned user rejoining"
|
||||
failReasonCode = EjectReasonCode.BANNED_USER_REJOINING
|
||||
} else if (u.loggedOut) {
|
||||
failReason = "User had logged out"
|
||||
failReasonCode = EjectReasonCode.USER_LOGGED_OUT
|
||||
@ -77,7 +77,8 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
|
||||
reasonCode: String,
|
||||
state: MeetingState2x
|
||||
): MeetingState2x = {
|
||||
val event = MsgBuilder.buildValidateAuthTokenRespMsg(meetingId, userId, authToken, valid, waitForApproval, 0, 0, Option.apply(reason))
|
||||
val event = MsgBuilder.buildValidateAuthTokenRespMsg(meetingId, userId, authToken, valid, waitForApproval, 0,
|
||||
0, reasonCode, reason)
|
||||
outGW.send(event)
|
||||
|
||||
// send a system message to force disconnection
|
||||
@ -88,8 +89,10 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
|
||||
}
|
||||
|
||||
def sendValidateAuthTokenRespMsg(meetingId: String, userId: String, authToken: String,
|
||||
valid: Boolean, waitForApproval: Boolean, registeredOn: Long, authTokenValidatedOn: Long, reason: Option[String] = None): Unit = {
|
||||
val event = MsgBuilder.buildValidateAuthTokenRespMsg(meetingId, userId, authToken, valid, waitForApproval, registeredOn, authTokenValidatedOn, reason)
|
||||
valid: Boolean, waitForApproval: Boolean, registeredOn: Long, authTokenValidatedOn: Long,
|
||||
reasonCode: String = EjectReasonCode.NOT_EJECT, reason: String = "User not ejected"): Unit = {
|
||||
val event = MsgBuilder.buildValidateAuthTokenRespMsg(meetingId, userId, authToken, valid, waitForApproval, registeredOn,
|
||||
authTokenValidatedOn, reasonCode, reason)
|
||||
outGW.send(event)
|
||||
}
|
||||
|
||||
|
@ -300,12 +300,13 @@ object SystemUser {
|
||||
}
|
||||
|
||||
object EjectReasonCode {
|
||||
val NOT_EJECT = "not_eject_reason"
|
||||
val DUPLICATE_USER = "duplicate_user_in_meeting_eject_reason"
|
||||
val PERMISSION_FAILED = "not_enough_permission_eject_reason"
|
||||
val EJECT_USER = "user_requested_eject_reason"
|
||||
val SYSTEM_EJECT_USER = "system_requested_eject_reason"
|
||||
val VALIDATE_TOKEN = "validate_token_failed_eject_reason"
|
||||
val USER_INACTIVITY = "user_inactivity_eject_reason"
|
||||
val EJECTED_USER_REJOINING = "ejected_user_rejoining_reason"
|
||||
val BANNED_USER_REJOINING = "banned_user_rejoining_reason"
|
||||
val USER_LOGGED_OUT = "user_logged_out_reason"
|
||||
}
|
||||
|
@ -85,11 +85,13 @@ object MsgBuilder {
|
||||
}
|
||||
|
||||
def buildValidateAuthTokenRespMsg(meetingId: String, userId: String, authToken: String,
|
||||
valid: Boolean, waitForApproval: Boolean, registeredOn: Long, authTokenValidatedOn: Long, reason: Option[String]): BbbCommonEnvCoreMsg = {
|
||||
valid: Boolean, waitForApproval: Boolean, registeredOn: Long, authTokenValidatedOn: Long,
|
||||
reasonCode: String, reason: String): BbbCommonEnvCoreMsg = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
|
||||
val envelope = BbbCoreEnvelope(ValidateAuthTokenRespMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(ValidateAuthTokenRespMsg.NAME, meetingId, userId)
|
||||
val body = ValidateAuthTokenRespMsgBody(userId, authToken, valid, waitForApproval, registeredOn, authTokenValidatedOn, reason)
|
||||
val body = ValidateAuthTokenRespMsgBody(userId, authToken, valid, waitForApproval, registeredOn, authTokenValidatedOn,
|
||||
reasonCode, reason)
|
||||
val event = ValidateAuthTokenRespMsg(header, body)
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
@ -6,11 +6,11 @@ import org.bigbluebutton.core.running.OutMsgRouter
|
||||
object ValidateAuthTokenRespMsgSender {
|
||||
|
||||
def send(outGW: OutMsgRouter, meetingId: String, userId: String, authToken: String,
|
||||
valid: Boolean, waitForApproval: Boolean, registeredOn: Long, authTokenValidatedOn: Long, reason: Option[String]): Unit = {
|
||||
valid: Boolean, waitForApproval: Boolean, registeredOn: Long, authTokenValidatedOn: Long, reasonCode: String, reason: String): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
|
||||
val envelope = BbbCoreEnvelope(ValidateAuthTokenRespMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(ValidateAuthTokenRespMsg.NAME, meetingId, userId)
|
||||
val body = ValidateAuthTokenRespMsgBody(userId, authToken, valid, waitForApproval, registeredOn, authTokenValidatedOn, reason)
|
||||
val body = ValidateAuthTokenRespMsgBody(userId, authToken, valid, waitForApproval, registeredOn, authTokenValidatedOn, reasonCode, reason)
|
||||
val event = ValidateAuthTokenRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
outGW.send(msgEvent)
|
||||
|
@ -60,7 +60,7 @@ case class ValidateAuthTokenRespMsg(
|
||||
body: ValidateAuthTokenRespMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class ValidateAuthTokenRespMsgBody(userId: String, authToken: String, valid: Boolean, waitForApproval: Boolean,
|
||||
registeredOn: Long, authTokenValidatedOn: Long, reason: Option[String])
|
||||
registeredOn: Long, authTokenValidatedOn: Long, reasonCode: String, reason: String)
|
||||
|
||||
object UserLeftMeetingEvtMsg {
|
||||
val NAME = "UserLeftMeetingEvtMsg"
|
@ -96,7 +96,7 @@ source /etc/bigbluebutton/bigbluebutton-release
|
||||
# Figure out our environment (Debian vs. CentOS)
|
||||
#
|
||||
|
||||
if [ -f /etc/centos-release ]; then
|
||||
if [ -f /etc/centos-release ] || [ -f /etc/system-release ]; then
|
||||
DISTRIB_ID=centos
|
||||
TOMCAT_USER=tomcat
|
||||
TOMCAT_DIR=/var/lib/$TOMCAT_USER
|
||||
@ -822,7 +822,7 @@ check_configuration() {
|
||||
echo
|
||||
echo "# Warning: No firewall detected. Recommend using setting up a firewall for your server"
|
||||
echo "#"
|
||||
echo "# https://docs.bigbluebutton.org/2.2/troubleshooting.html#freeswitch-using-default-stun-server"
|
||||
echo "# https://docs.bigbluebutton.org/2.2/customize.html#setup-a-firewall"
|
||||
echo "#"
|
||||
echo
|
||||
fi
|
||||
|
@ -9,7 +9,7 @@ if [ ! -f /var/tmp/bbb-kms-last-restart.txt ]; then
|
||||
exit
|
||||
fi
|
||||
|
||||
users=$(mongo --quiet mongodb://127.0.1.1:27017/meteor --eval "db.users.count({connectionStatus: 'online'})")
|
||||
users=$(mongo --quiet mongodb://127.0.1.1:27017/meteor --eval "db.users.count()")
|
||||
|
||||
if [ "$users" -eq 0 ]; then
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import addConnectionStatus from './methods/addConnectionStatus';
|
||||
import voidConnection from './methods/voidConnection';
|
||||
|
||||
Meteor.methods({
|
||||
addConnectionStatus,
|
||||
voidConnection,
|
||||
});
|
||||
|
@ -7,5 +7,8 @@ export default function addConnectionStatus(level) {
|
||||
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
updateConnectionStatus(meetingId, requesterUserId, level);
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
// Round-trip time helper
|
||||
export default function voidConnection() {
|
||||
return 0;
|
||||
}
|
@ -2,13 +2,25 @@ import ConnectionStatus from '/imports/api/connection-status';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function clearConnectionStatus(meetingId) {
|
||||
const selector = {};
|
||||
|
||||
if (meetingId) {
|
||||
return ConnectionStatus.remove({ meetingId }, () => {
|
||||
Logger.info(`Cleared ConnectionStatus (${meetingId})`);
|
||||
});
|
||||
selector.meetingId = meetingId;
|
||||
}
|
||||
|
||||
return ConnectionStatus.remove({}, () => {
|
||||
Logger.info('Cleared ConnectionStatus (all)');
|
||||
});
|
||||
try {
|
||||
const numberAffected = ConnectionStatus.remove(selector);
|
||||
|
||||
if (numberAffected) {
|
||||
if (meetingId) {
|
||||
Logger.info(`Removed ConnectionStatus (${meetingId})`);
|
||||
} else {
|
||||
Logger.info('Removed ConnectionStatus (all)');
|
||||
}
|
||||
} else {
|
||||
Logger.warn('Removing ConnectionStatus nonaffected');
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Removing ConnectionStatus: ${err}`);
|
||||
}
|
||||
}
|
||||
|
@ -20,18 +20,13 @@ export default function updateConnectionStatus(meetingId, userId, level) {
|
||||
timestamp,
|
||||
};
|
||||
|
||||
const cb = (err, numChanged) => {
|
||||
if (err) {
|
||||
return Logger.error(`Updating connection status: ${err}`);
|
||||
}
|
||||
try {
|
||||
const { numberAffected } = ConnectionStatus.upsert(selector, modifier);
|
||||
|
||||
const { insertedId } = numChanged;
|
||||
if (insertedId) {
|
||||
return Logger.info(`Added connection status userId=${userId} level=${level}`);
|
||||
if (numberAffected) {
|
||||
Logger.verbose(`Updated connection status userId=${userId} level=${level}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Updating connection status: ${err}`);
|
||||
}
|
||||
|
||||
return Logger.verbose(`Update connection status userId=${userId} level=${level}`);
|
||||
};
|
||||
|
||||
return ConnectionStatus.upsert(selector, modifier, cb);
|
||||
}
|
||||
|
@ -7,14 +7,22 @@ export default async function addBulkGroupChatMsgs(msgs) {
|
||||
if (!msgs.length) return;
|
||||
|
||||
const mappedMsgs = msgs
|
||||
.map(({ chatId, meetingId, msg }) => ({
|
||||
.map(({ chatId, meetingId, msg }) => {
|
||||
const {
|
||||
sender,
|
||||
color,
|
||||
...restMsg
|
||||
} = msg;
|
||||
|
||||
return {
|
||||
_id: new Mongo.ObjectID()._str,
|
||||
...msg,
|
||||
...restMsg,
|
||||
meetingId,
|
||||
chatId,
|
||||
message: parseMessage(msg.message),
|
||||
sender: msg.sender.id,
|
||||
}))
|
||||
sender: sender.id,
|
||||
};
|
||||
})
|
||||
.map(el => flat(el, { safe: true }));
|
||||
|
||||
try {
|
||||
|
@ -28,8 +28,16 @@ export default function addGroupChatMsg(meetingId, chatId, msg) {
|
||||
message: String,
|
||||
correlationId: Match.Maybe(String),
|
||||
});
|
||||
|
||||
const {
|
||||
color,
|
||||
sender,
|
||||
...restMsg
|
||||
} = msg;
|
||||
|
||||
const msgDocument = {
|
||||
...msg,
|
||||
...restMsg,
|
||||
sender: sender.id,
|
||||
meetingId,
|
||||
chatId,
|
||||
message: parseMessage(msg.message),
|
||||
|
@ -16,12 +16,18 @@ export default function syncMeetingChatMsgs(meetingId, chatId, msgs) {
|
||||
|
||||
msgs
|
||||
.forEach((msg) => {
|
||||
const {
|
||||
sender,
|
||||
color,
|
||||
...restMsg
|
||||
} = msg;
|
||||
|
||||
const msgToSync = {
|
||||
...msg,
|
||||
...restMsg,
|
||||
meetingId,
|
||||
chatId,
|
||||
message: parseMessage(msg.message),
|
||||
sender: msg.sender.id,
|
||||
sender: sender.id,
|
||||
};
|
||||
|
||||
const modifier = flat(msgToSync, { safe: true });
|
||||
|
@ -28,6 +28,8 @@ import clearRecordMeeting from './clearRecordMeeting';
|
||||
import clearVoiceCallStates from '/imports/api/voice-call-states/server/modifiers/clearVoiceCallStates';
|
||||
import clearVideoStreams from '/imports/api/video-streams/server/modifiers/clearVideoStreams';
|
||||
import clearAuthTokenValidation from '/imports/api/auth-token-validation/server/modifiers/clearAuthTokenValidation';
|
||||
import clearUsersPersistentData from '/imports/api/users-persistent-data/server/modifiers/clearUsersPersistentData';
|
||||
|
||||
import clearWhiteboardMultiUser from '/imports/api/whiteboard-multi-user/server/modifiers/clearWhiteboardMultiUser';
|
||||
import Metrics from '/imports/startup/server/metrics';
|
||||
|
||||
@ -62,6 +64,7 @@ export default function meetingHasEnded(meetingId) {
|
||||
clearAuthTokenValidation(meetingId);
|
||||
clearWhiteboardMultiUser(meetingId);
|
||||
clearScreenshare(meetingId);
|
||||
clearUsersPersistentData(meetingId);
|
||||
BannedUsers.delete(meetingId);
|
||||
Metrics.removeMeeting(meetingId);
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const UsersPersistentData = new Mongo.Collection('users-persistent-data');
|
||||
|
||||
if (Meteor.isServer) {
|
||||
UsersPersistentData._ensureIndex({ meetingId: 1, userId: 1 });
|
||||
}
|
||||
|
||||
export default UsersPersistentData;
|
@ -0,0 +1 @@
|
||||
import './publishers';
|
@ -0,0 +1,76 @@
|
||||
import { check } from 'meteor/check';
|
||||
import UsersPersistentData from '/imports/api/users-persistent-data';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function addUserPersistentData(user) {
|
||||
check(user, {
|
||||
meetingId: String,
|
||||
sortName: String,
|
||||
color: String,
|
||||
mobile: Boolean,
|
||||
breakoutProps: Object,
|
||||
inactivityCheck: Boolean,
|
||||
responseDelay: Number,
|
||||
loggedOut: Boolean,
|
||||
intId: String,
|
||||
extId: String,
|
||||
name: String,
|
||||
role: String,
|
||||
guest: Boolean,
|
||||
authed: Boolean,
|
||||
guestStatus: String,
|
||||
emoji: String,
|
||||
presenter: Boolean,
|
||||
locked: Boolean,
|
||||
avatar: String,
|
||||
clientType: String,
|
||||
effectiveConnectionType: null,
|
||||
});
|
||||
|
||||
|
||||
const {
|
||||
intId,
|
||||
extId,
|
||||
meetingId,
|
||||
name,
|
||||
role,
|
||||
token,
|
||||
avatar,
|
||||
guest,
|
||||
color,
|
||||
} = user;
|
||||
|
||||
const userData = {
|
||||
userId: intId,
|
||||
extId,
|
||||
meetingId,
|
||||
name,
|
||||
role,
|
||||
token,
|
||||
avatar,
|
||||
guest,
|
||||
color,
|
||||
loggedOut: false,
|
||||
};
|
||||
|
||||
const selector = {
|
||||
userId: intId,
|
||||
meetingId,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: userData,
|
||||
};
|
||||
|
||||
try {
|
||||
const { insertedId } = UsersPersistentData.upsert(selector, modifier);
|
||||
|
||||
if (insertedId) {
|
||||
Logger.info(`Added user id=${intId} to user persistent Data: meeting=${meetingId}`);
|
||||
} else {
|
||||
Logger.info(`Upserted user id=${intId} to user persistent Data: meeting=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Adding note to the collection: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import UsersPersistentData from '/imports/api/users-persistent-data/index';
|
||||
|
||||
export default function clearUsersPersistentData(meetingId) {
|
||||
if (meetingId) {
|
||||
try {
|
||||
const numberAffected = UsersPersistentData.remove({ meetingId });
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info(`Cleared users persistent data (${meetingId})`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error clearing users persistent data (${meetingId}). ${err}`);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const numberAffected = UsersPersistentData.remove({});
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info('Cleared users persistent data (all)');
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error clearing users persistent data (all). ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { check } from 'meteor/check';
|
||||
import UsersPersistentData from '/imports/api/users-persistent-data';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function setloggedOutStatus(userId, meetingId, status = true) {
|
||||
check(userId, String);
|
||||
check(meetingId, String);
|
||||
check(status, Boolean);
|
||||
|
||||
const selector = {
|
||||
userId,
|
||||
meetingId,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
loggedOut: status,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
UsersPersistentData.update(selector, modifier);
|
||||
} catch (err) {
|
||||
Logger.error(`Adding note to the collection: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import UsersPersistentData from '/imports/api/users-persistent-data';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
function usersPersistentData() {
|
||||
if (!this.userId) {
|
||||
return UsersPersistentData.find({ meetingId: '' });
|
||||
}
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
};
|
||||
|
||||
const options = {
|
||||
fields: {
|
||||
meetingId: false,
|
||||
},
|
||||
};
|
||||
|
||||
return UsersPersistentData.find(selector, options);
|
||||
}
|
||||
|
||||
function publishUsersPersistentData(...args) {
|
||||
const boundUsers = usersPersistentData.bind(this);
|
||||
return boundUsers(...args);
|
||||
}
|
||||
|
||||
Meteor.publish('users-persistent-data', publishUsersPersistentData);
|
@ -5,7 +5,7 @@ import Meetings from '/imports/api/meetings';
|
||||
import VoiceUsers from '/imports/api/voice-users/';
|
||||
import _ from 'lodash';
|
||||
import SanitizeHTML from 'sanitize-html';
|
||||
|
||||
import addUserPsersistentData from '/imports/api/users-persistent-data/server/modifiers/addUserPersistentData';
|
||||
import stringHash from 'string-hash';
|
||||
import flat from 'flat';
|
||||
|
||||
@ -58,8 +58,7 @@ export default function addUser(meetingId, userData) {
|
||||
from a list based on the userId */
|
||||
const color = COLOR_LIST[stringHash(user.intId) % COLOR_LIST.length];
|
||||
|
||||
const modifier = {
|
||||
$set: Object.assign(
|
||||
const userInfos = Object.assign(
|
||||
{
|
||||
meetingId,
|
||||
sortName: user.name.trim().toLowerCase(),
|
||||
@ -75,9 +74,12 @@ export default function addUser(meetingId, userData) {
|
||||
loggedOut: false,
|
||||
},
|
||||
flat(user),
|
||||
),
|
||||
};
|
||||
);
|
||||
|
||||
const modifier = {
|
||||
$set: userInfos,
|
||||
};
|
||||
addUserPsersistentData(userInfos);
|
||||
// Only add an empty VoiceUser if there isn't one already and if the user coming in isn't a
|
||||
// dial-in user. We want to avoid overwriting good data
|
||||
if (user.clientType !== 'dial-in-user' && !VoiceUsers.findOne({ meetingId, intId: userId })) {
|
||||
|
@ -2,6 +2,7 @@ import { check } from 'meteor/check';
|
||||
import Users from '/imports/api/users';
|
||||
import VideoStreams from '/imports/api/video-streams';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import setloggedOutStatus from '/imports/api/users-persistent-data/server/modifiers/setloggedOutStatus';
|
||||
import stopWatchingExternalVideoSystemCall from '/imports/api/external-videos/server/methods/stopWatchingExternalVideoSystemCall';
|
||||
import clearUserInfoForRequester from '/imports/api/users-infos/server/modifiers/clearUserInfoForRequester';
|
||||
import ClientConnections from '/imports/startup/server/ClientConnections';
|
||||
@ -32,6 +33,7 @@ export default function removeUser(meetingId, userId) {
|
||||
};
|
||||
|
||||
try {
|
||||
setloggedOutStatus(userId, meetingId, true);
|
||||
VideoStreams.remove({ meetingId, userId });
|
||||
const sessionUserId = `${meetingId}-${userId}`;
|
||||
|
||||
|
@ -26,6 +26,7 @@ import RandomUserSelectContainer from '/imports/ui/components/modal/random-user/
|
||||
import { withDraggableContext } from '../media/webcam-draggable-overlay/context';
|
||||
import { styles } from './styles';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
|
||||
import { NAVBAR_HEIGHT } from '/imports/ui/components/layout/layout-manager';
|
||||
|
||||
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
||||
@ -161,6 +162,8 @@ class App extends Component {
|
||||
|
||||
if (isMobileBrowser) makeCall('setMobileUser');
|
||||
|
||||
ConnectionStatusService.startRoundTripTime();
|
||||
|
||||
logger.info({ logCode: 'app_component_componentdidmount' }, 'Client loaded successfully');
|
||||
}
|
||||
|
||||
@ -226,6 +229,8 @@ class App extends Component {
|
||||
if (navigator.connection) {
|
||||
navigator.connection.addEventListener('change', handleNetworkConnection, false);
|
||||
}
|
||||
|
||||
ConnectionStatusService.stopRoundTripTime();
|
||||
}
|
||||
|
||||
handleWindowResize() {
|
||||
|
@ -109,6 +109,7 @@ class BreakoutJoinConfirmation extends Component {
|
||||
}, 'joining breakout room closed audio in the main room');
|
||||
}
|
||||
|
||||
VideoService.storeDeviceIds();
|
||||
VideoService.exitVideo();
|
||||
if (UserListService.amIPresenter()) screenshareHasEnded();
|
||||
if (url === '') {
|
||||
|
@ -248,6 +248,7 @@ class BreakoutRoom extends PureComponent {
|
||||
logCode: 'breakoutroom_join',
|
||||
extraInfo: { logType: 'user_action' },
|
||||
}, 'joining breakout room closed audio in the main room');
|
||||
VideoService.storeDeviceIds();
|
||||
VideoService.exitVideo();
|
||||
if (UserListService.amIPresenter()) screenshareHasEnded();
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ const SIZES = [
|
||||
];
|
||||
|
||||
const COLORS = [
|
||||
'default', 'primary', 'danger', 'success', 'dark',
|
||||
'default', 'primary', 'danger', 'warning', 'success', 'dark', 'offline',
|
||||
];
|
||||
|
||||
const propTypes = {
|
||||
|
@ -18,6 +18,10 @@
|
||||
--btn-success-bg: var(--color-success);
|
||||
--btn-success-border: var(--color-success);
|
||||
|
||||
--btn-warning-color: var(--color-white);
|
||||
--btn-warning-bg: var(--color-warning);
|
||||
--btn-warning-border: var(--color-warning);
|
||||
|
||||
--btn-danger-color: var(--color-white);
|
||||
--btn-danger-bg: var(--color-danger);
|
||||
--btn-danger-border: var(--color-danger);
|
||||
@ -26,6 +30,10 @@
|
||||
--btn-dark-bg: var(--color-gray-dark);
|
||||
--btn-dark-border: var(--color-danger);
|
||||
|
||||
--btn-offline-color: var(--color-white);
|
||||
--btn-offline-bg: var(--color-offline);
|
||||
--btn-offline-border: var(--color-offline);
|
||||
|
||||
--btn-border-size: var(--border-size);
|
||||
--btn-border-radius: var(--border-radius);
|
||||
--btn-font-weight: 600;
|
||||
@ -258,6 +266,10 @@
|
||||
@include button-variant(var(--btn-success-color), var(--btn-success-bg), var(--btn-success-border));
|
||||
}
|
||||
|
||||
.warning {
|
||||
@include button-variant(var(--btn-warning-color), var(--btn-warning-bg), var(--btn-warning-border));
|
||||
}
|
||||
|
||||
.danger {
|
||||
@include button-variant(var(--btn-danger-color), var(--btn-danger-bg), var(--btn-danger-border));
|
||||
}
|
||||
@ -266,6 +278,10 @@
|
||||
@include button-variant(var(--btn-dark-color), var(--btn-dark-bg), var(--btn-dark-border));
|
||||
}
|
||||
|
||||
.offline {
|
||||
@include button-variant(var(--btn-offline-color), var(--btn-offline-bg), var(--btn-offline-border));
|
||||
}
|
||||
|
||||
/* Styles
|
||||
* ==========
|
||||
*/
|
||||
@ -288,6 +304,10 @@
|
||||
@include button-ghost-variant(var(--btn-success-bg), var(--btn-success-color));
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@include button-ghost-variant(var(--btn-warning-bg), var(--btn-warning-color));
|
||||
}
|
||||
|
||||
&.danger {
|
||||
@include button-ghost-variant(var(--btn-danger-bg), var(--btn-danger-color));
|
||||
}
|
||||
@ -295,6 +315,10 @@
|
||||
&.dark {
|
||||
@include button-ghost-variant(var(--btn-dark-bg), var(--btn-dark-color));
|
||||
}
|
||||
|
||||
&.offline {
|
||||
@include button-ghost-variant(var(--btn-offline-bg), var(--btn-offline-color));
|
||||
}
|
||||
}
|
||||
|
||||
.circle {
|
||||
|
@ -55,11 +55,11 @@ class ChatDropdown extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { timeWindowsValues } = this.props;
|
||||
const { timeWindowsValues, users } = this.props;
|
||||
const { isSettingOpen } = this.state;
|
||||
if (prevState.isSettingOpen !== isSettingOpen) {
|
||||
this.clipboard = new Clipboard('#clipboardButton', {
|
||||
text: () => ChatService.exportChat(timeWindowsValues),
|
||||
text: () => ChatService.exportChat(timeWindowsValues, users),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -82,7 +82,7 @@ class ChatDropdown extends PureComponent {
|
||||
|
||||
getAvailableActions() {
|
||||
const {
|
||||
intl, isMeteorConnected, amIModerator, meetingIsBreakout, meetingName, timeWindowsValues,
|
||||
intl, isMeteorConnected, amIModerator, meetingIsBreakout, meetingName, timeWindowsValues, users,
|
||||
} = this.props;
|
||||
|
||||
const clearIcon = 'delete';
|
||||
@ -104,7 +104,7 @@ class ChatDropdown extends PureComponent {
|
||||
link.setAttribute(
|
||||
'href',
|
||||
`data: ${mimeType} ;charset=utf-8,
|
||||
${encodeURIComponent(ChatService.exportChat(timeWindowsValues))}`,
|
||||
${encodeURIComponent(ChatService.exportChat(timeWindowsValues, users))}`,
|
||||
);
|
||||
link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
||||
}}
|
||||
|
@ -1,10 +1,16 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||
import ChatDropdown from './component';
|
||||
|
||||
const ChatDropdownContainer = ({ ...props }) => <ChatDropdown {...props} />;
|
||||
const ChatDropdownContainer = ({ ...props }) => {
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
const { users } = usingUsersContext;
|
||||
|
||||
return <ChatDropdown {...props} users={users} />;
|
||||
};
|
||||
|
||||
export default withTracker(() => {
|
||||
const getMeetingName = () => {
|
||||
|
@ -167,6 +167,11 @@ const ChatContainer = (props) => {
|
||||
globalAppplyStateToProps = applyPropsToState;
|
||||
throttledFunc();
|
||||
|
||||
ChatService.removePackagedClassAttribute(
|
||||
["ReactVirtualized__Grid", "ReactVirtualized__Grid__innerScrollContainer"],
|
||||
"role"
|
||||
);
|
||||
|
||||
return (
|
||||
<Chat {...{
|
||||
...props,
|
||||
|
@ -286,10 +286,8 @@ class MessageForm extends PureComponent {
|
||||
id="message-input"
|
||||
innerRef={(ref) => { this.textarea = ref; return this.textarea; }}
|
||||
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })}
|
||||
aria-controls={chatAreaId}
|
||||
aria-label={intl.formatMessage(messages.inputLabel, { 0: chatTitle })}
|
||||
aria-invalid={hasErrors ? 'true' : 'false'}
|
||||
aria-describedby={hasErrors ? 'message-input-error' : null}
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
spellCheck="true"
|
||||
@ -297,7 +295,7 @@ class MessageForm extends PureComponent {
|
||||
value={message}
|
||||
onChange={this.handleMessageChange}
|
||||
onKeyDown={this.handleMessageKeyDown}
|
||||
async={true}
|
||||
async
|
||||
/>
|
||||
<Button
|
||||
hideLabel
|
||||
|
@ -312,21 +312,28 @@ const htmlDecode = (input) => {
|
||||
};
|
||||
|
||||
// Export the chat as [Hour:Min] user: message
|
||||
const exportChat = (timeWindowList) => {
|
||||
const messageList = timeWindowList.reduce( (acc, timeWindow) => [...acc, ...timeWindow.content], []);
|
||||
messageList.sort((a, b) => a.time - b.time);
|
||||
const exportChat = (timeWindowList, users) => {
|
||||
// const messageList = timeWindowList.reduce( (acc, timeWindow) => [...acc, ...timeWindow.content], []);
|
||||
// messageList.sort((a, b) => a.time - b.time);
|
||||
|
||||
return messageList.map(message => {
|
||||
const messageList = timeWindowList.reduce((acc, timeWindow) => {
|
||||
|
||||
const msgs = timeWindow.content.map(message => {
|
||||
const date = new Date(message.time);
|
||||
const hour = date.getHours().toString().padStart(2, 0);
|
||||
const min = date.getMinutes().toString().padStart(2, 0);
|
||||
const hourMin = `[${hour}:${min}]`;
|
||||
|
||||
console.log('message', message);
|
||||
const userName = message.id.endsWith('welcome-msg')
|
||||
? ''
|
||||
: `${message.name} :`;
|
||||
: `${users[timeWindow.sender].name} :`;
|
||||
return `${hourMin} ${userName} ${htmlDecode(message.text)}`;
|
||||
}).join('\n');
|
||||
});
|
||||
|
||||
return [...acc, ...msgs];
|
||||
}, [])
|
||||
|
||||
return messageList.join('\n');
|
||||
}
|
||||
|
||||
|
||||
@ -360,6 +367,17 @@ const getLastMessageTimestampFromChatList = activeChats => activeChats
|
||||
.map(chatId => getAllMessages(chatId).reduce(maxTimestampReducer, 0))
|
||||
.reduce(maxNumberReducer, 0);
|
||||
|
||||
const removePackagedClassAttribute = (classnames, attribute) => {
|
||||
classnames.map(c => {
|
||||
const elements = document.getElementsByClassName(c);
|
||||
if (elements) {
|
||||
for (const [k, v] of Object.entries(elements)) {
|
||||
v.removeAttribute(attribute);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
setUserSentMessage,
|
||||
mapGroupMessage,
|
||||
@ -385,4 +403,5 @@ export default {
|
||||
maxTimestampReducer,
|
||||
getLastMessageTimestampFromChatList,
|
||||
UnsentMessagesCollection,
|
||||
removePackagedClassAttribute,
|
||||
};
|
||||
|
@ -40,21 +40,19 @@ export default function TimeWindowChatItemContainer(props) {
|
||||
key,
|
||||
timestamp,
|
||||
content,
|
||||
color,
|
||||
} = message;
|
||||
|
||||
const messages = content;
|
||||
const user = users[sender?.id];
|
||||
const user = users[sender];
|
||||
const messageKey = key;
|
||||
return (
|
||||
<TimeWindowChatItem
|
||||
{
|
||||
...{
|
||||
color: user?.color || color,
|
||||
color: user?.color,
|
||||
isModerator: user?.role === ROLE_MODERATOR,
|
||||
isOnline: !!user,
|
||||
isOnline: !user?.loggedOut,
|
||||
avatar: user?.avatar,
|
||||
name: user?.name || sender?.name,
|
||||
name: user?.name,
|
||||
read: message.read,
|
||||
messages,
|
||||
isDefaultPoll,
|
||||
|
@ -46,11 +46,11 @@ export const ChatContext = createContext();
|
||||
const generateStateWithNewMessage = (msg, state) => {
|
||||
|
||||
const timeWindow = generateTimeWindow(msg.timestamp);
|
||||
const userId = msg.sender.id;
|
||||
const userId = msg.sender;
|
||||
const keyName = userId + '-' + timeWindow;
|
||||
const msgBuilder = (msg, chat) => {
|
||||
const msgTimewindow = generateTimeWindow(msg.timestamp);
|
||||
const key = msg.sender.id + '-' + msgTimewindow;
|
||||
const key = msg.sender + '-' + msgTimewindow;
|
||||
const chatIndex = chat?.chatIndexes[key];
|
||||
const {
|
||||
_id,
|
||||
@ -66,7 +66,7 @@ const generateStateWithNewMessage = (msg, state) => {
|
||||
lastTimestamp: msg.timestamp,
|
||||
read: msg.chatId === PUBLIC_CHAT_KEY && msg.timestamp <= getLoginTime() ? true : false,
|
||||
content: [
|
||||
{ id: msg.id, name: msg.sender.name, text: msg.message, time: msg.timestamp },
|
||||
{ id: msg.id, text: msg.message, time: msg.timestamp },
|
||||
],
|
||||
}
|
||||
};
|
||||
@ -109,7 +109,7 @@ const generateStateWithNewMessage = (msg, state) => {
|
||||
const timewindowIndex = stateMessages.chatIndexes[keyName];
|
||||
const groupMessage = messageGroups[keyName + '-' + timewindowIndex];
|
||||
|
||||
if (!groupMessage || (groupMessage && groupMessage.sender.id !== stateMessages.lastSender.id)) {
|
||||
if (!groupMessage || (groupMessage && groupMessage.sender !== stateMessages.lastSender)) {
|
||||
|
||||
const [tempGroupMessage, sender, newIndex] = msgBuilder(msg, stateMessages);
|
||||
stateMessages.lastSender = sender;
|
||||
@ -122,13 +122,13 @@ const generateStateWithNewMessage = (msg, state) => {
|
||||
messageGroups[key] = tempGroupMessage[key];
|
||||
const message = tempGroupMessage[key];
|
||||
const previousMessage = message.timestamp <= getLoginTime();
|
||||
if (!previousMessage && message.sender.id !== Auth.userID && !message.id.startsWith(SYSTEM_CHAT_TYPE)) {
|
||||
if (!previousMessage && message.sender !== Auth.userID && !message.id.startsWith(SYSTEM_CHAT_TYPE)) {
|
||||
stateMessages.unreadTimeWindows.add(key);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (groupMessage) {
|
||||
if (groupMessage.sender.id === stateMessages.lastSender.id) {
|
||||
if (groupMessage.sender === stateMessages.lastSender) {
|
||||
const previousMessage = msg.timestamp <= getLoginTime();
|
||||
const timeWindowKey = keyName + '-' + stateMessages.chatIndexes[keyName];
|
||||
messageGroups[timeWindowKey] = {
|
||||
@ -137,10 +137,10 @@ const generateStateWithNewMessage = (msg, state) => {
|
||||
read: previousMessage ? true : false,
|
||||
content: [
|
||||
...groupMessage.content,
|
||||
{ id: msg.id, name: groupMessage.sender.name, text: msg.message, time: msg.timestamp }
|
||||
{ id: msg.id, text: msg.message, time: msg.timestamp }
|
||||
],
|
||||
};
|
||||
if (!previousMessage && groupMessage.sender.id !== Auth.userID) {
|
||||
if (!previousMessage && groupMessage.sender !== Auth.userID) {
|
||||
stateMessages.unreadTimeWindows.add(timeWindowKey);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useContext, useEffect } from 'react';
|
||||
import Users from '/imports/api/users';
|
||||
import UsersPersistentData from '/imports/api/users-persistent-data';
|
||||
import { UsersContext, ACTIONS } from './context';
|
||||
import { ChatContext, ACTIONS as CHAT_ACTIONS } from '../chat-context/context';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
@ -8,8 +9,30 @@ const Adapter = () => {
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
const { dispatch } = usingUsersContext;
|
||||
|
||||
const usingChatContext = useContext(ChatContext);
|
||||
const { dispatch: chatDispatch } = usingChatContext;
|
||||
useEffect(()=> {
|
||||
const usersPersistentDataCursor = UsersPersistentData.find({}, { sort: { timestamp: 1 } });
|
||||
usersPersistentDataCursor.observe({
|
||||
added: (obj) => {
|
||||
ChatLogger.debug("usersAdapter::observe::added_persistent_user", obj);
|
||||
dispatch({
|
||||
type: ACTIONS.ADDED_USER_PERSISTENT_DATA,
|
||||
value: {
|
||||
user: obj,
|
||||
},
|
||||
});
|
||||
},
|
||||
changed: (obj) => {
|
||||
ChatLogger.debug("usersAdapter::observe::changed_persistent_user", obj);
|
||||
dispatch({
|
||||
type: ACTIONS.CHANGED_USER_PERSISTENT_DATA,
|
||||
value: {
|
||||
user: obj,
|
||||
},
|
||||
});
|
||||
},
|
||||
removed: (obj) => {},
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const usersCursor = Users.find({}, { sort: { timestamp: 1 } });
|
||||
@ -31,14 +54,6 @@ const Adapter = () => {
|
||||
},
|
||||
});
|
||||
},
|
||||
removed: (obj) => {
|
||||
dispatch({
|
||||
type: ACTIONS.REMOVED,
|
||||
value: {
|
||||
user: obj,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
@ -9,6 +9,8 @@ export const ACTIONS = {
|
||||
ADDED: 'added',
|
||||
CHANGED: 'changed',
|
||||
REMOVED: 'removed',
|
||||
ADDED_USER_PERSISTENT_DATA: 'added_user_persistent_data',
|
||||
CHANGED_USER_PERSISTENT_DATA: 'changed_user_persistent_data',
|
||||
};
|
||||
|
||||
export const UsersContext = createContext();
|
||||
@ -47,6 +49,37 @@ const reducer = (state, action) => {
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
//USER PERSISTENT DATA
|
||||
case ACTIONS.ADDED_USER_PERSISTENT_DATA: {
|
||||
const { user } = action.value;
|
||||
if (state[user.userId]) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
[user.userId]: {
|
||||
...user,
|
||||
},
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
case ACTIONS.CHANGED_USER_PERSISTENT_DATA: {
|
||||
const { user } = action.value;
|
||||
const stateUser = state[user.userId];
|
||||
if (stateUser) {
|
||||
const newState = {
|
||||
...state,
|
||||
[user.userId]: {
|
||||
...stateUser,
|
||||
...user,
|
||||
},
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unexpected action: ${JSON.stringify(action)}`);
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import ConnectionStatusModalContainer from '/imports/ui/components/connection-status/modal/container';
|
||||
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
|
||||
import { styles } from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
label: {
|
||||
id: 'app.connection-status.label',
|
||||
description: 'Connection status button label',
|
||||
},
|
||||
description: {
|
||||
id: 'app.connection-status.description',
|
||||
description: 'Connection status button description',
|
||||
},
|
||||
});
|
||||
|
||||
class ConnectionStatusButton extends Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const {
|
||||
connected,
|
||||
stats,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
connected: nextConnected,
|
||||
stats: nextStats,
|
||||
} = nextProps;
|
||||
|
||||
// Always re-render when the connection state change
|
||||
if (connected !== nextConnected) return true;
|
||||
|
||||
// Avoid simple re-render case
|
||||
if (stats === nextStats) return false;
|
||||
|
||||
// Avoid updating when component drifts between danger and critical
|
||||
// since it's the same feedback
|
||||
if (stats === 'danger' && nextStats === 'critical') return false;
|
||||
if (stats === 'critical' && nextStats === 'danger') return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
connected,
|
||||
stats,
|
||||
mountModal,
|
||||
} = this.props;
|
||||
|
||||
let color;
|
||||
if (!connected) {
|
||||
color = 'offline';
|
||||
} else {
|
||||
switch (stats) {
|
||||
case 'warning':
|
||||
case 'danger':
|
||||
// We map warning and danger stats into the warning palette color and
|
||||
// the error notification
|
||||
color = 'warning';
|
||||
ConnectionStatusService.notification('warning', intl);
|
||||
break;
|
||||
case 'critical':
|
||||
color = 'danger';
|
||||
ConnectionStatusService.notification('error', intl);
|
||||
break;
|
||||
default:
|
||||
color = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon="network"
|
||||
label={intl.formatMessage(intlMessages.label)}
|
||||
hideLabel
|
||||
aria-label={intl.formatMessage(intlMessages.description)}
|
||||
size="sm"
|
||||
color={color}
|
||||
disabled={!connected}
|
||||
circle
|
||||
onClick={() => mountModal(<ConnectionStatusModalContainer />)}
|
||||
data-test="connectionStatusButton"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(withModalMounter(ConnectionStatusButton));
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import ConnectionStatusService from '../service';
|
||||
import ConnectionStatusButtonComponent from './component';
|
||||
|
||||
const connectionStatusButtonContainer = props => <ConnectionStatusButtonComponent {...props} />;
|
||||
|
||||
export default withTracker(() => {
|
||||
const { connected } = Meteor.status();
|
||||
|
||||
return {
|
||||
connected,
|
||||
stats: ConnectionStatusService.getStats(),
|
||||
};
|
||||
})(connectionStatusButtonContainer);
|
@ -0,0 +1,17 @@
|
||||
.btn {
|
||||
margin: 0;
|
||||
|
||||
span {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
span {
|
||||
background-color: transparent !important;
|
||||
color: var(--color-white) !important;
|
||||
opacity: .75;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,11 +4,11 @@ import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import UserAvatar from '/imports/ui/components/user-avatar/component';
|
||||
import SlowConnection from '/imports/ui/components/slow-connection/component';
|
||||
import Switch from '/imports/ui/components/switch/component';
|
||||
import Service from '../service';
|
||||
import Modal from '/imports/ui/components/modal/simple/component';
|
||||
import { styles } from './styles';
|
||||
|
||||
const STATS = Meteor.settings.public.stats;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
ariaTitle: {
|
||||
id: 'app.connection-status.ariaTitle',
|
||||
@ -30,9 +30,17 @@ const intlMessages = defineMessages({
|
||||
id: 'app.connection-status.more',
|
||||
description: 'More about conectivity issues',
|
||||
},
|
||||
offline: {
|
||||
id: 'app.connection-status.offline',
|
||||
description: 'Offline user',
|
||||
dataSaving: {
|
||||
id: 'app.settings.dataSavingTab.description',
|
||||
description: 'Description of data saving',
|
||||
},
|
||||
webcam: {
|
||||
id: 'app.settings.dataSavingTab.webcam',
|
||||
description: 'Webcam data saving switch',
|
||||
},
|
||||
screenshare: {
|
||||
id: 'app.settings.dataSavingTab.screenShare',
|
||||
description: 'Screenshare data saving switch',
|
||||
},
|
||||
});
|
||||
|
||||
@ -43,7 +51,33 @@ const propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
const isConnectionStatusEmpty = (connectionStatus) => {
|
||||
// Check if it's defined
|
||||
if (!connectionStatus) return true;
|
||||
|
||||
// Check if it's an array
|
||||
if (!Array.isArray(connectionStatus)) return true;
|
||||
|
||||
// Check if is empty
|
||||
if (connectionStatus.length === 0) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
class ConnectionStatusComponent extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.help = Service.getHelp();
|
||||
this.state = { dataSaving: props.dataSaving };
|
||||
}
|
||||
|
||||
handleDataSavingChange(key) {
|
||||
const { dataSaving } = this.state;
|
||||
dataSaving[key] = !dataSaving[key];
|
||||
this.setState(dataSaving);
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
const { intl } = this.props;
|
||||
|
||||
@ -66,15 +100,12 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
if (connectionStatus.length === 0) return this.renderEmpty();
|
||||
if (isConnectionStatusEmpty(connectionStatus)) return this.renderEmpty();
|
||||
|
||||
return connectionStatus.map((conn, index) => {
|
||||
const dateTime = new Date(conn.timestamp);
|
||||
const itemStyle = {};
|
||||
itemStyle[styles.even] = index % 2 === 0;
|
||||
|
||||
const textStyle = {};
|
||||
textStyle[styles.offline] = conn.offline;
|
||||
itemStyle[styles.even] = (index + 1) % 2 === 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -95,9 +126,8 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
</div>
|
||||
|
||||
<div className={styles.name}>
|
||||
<div className={cx(styles.text, textStyle)}>
|
||||
<div className={styles.text}>
|
||||
{conn.name}
|
||||
{conn.offline ? ` (${intl.formatMessage(intlMessages.offline)})` : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.status}>
|
||||
@ -116,19 +146,63 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
renderDataSaving() {
|
||||
const {
|
||||
intl,
|
||||
dataSaving,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
viewParticipantsWebcams,
|
||||
viewScreenshare,
|
||||
} = dataSaving;
|
||||
|
||||
return (
|
||||
<div className={styles.dataSaving}>
|
||||
<div className={styles.description}>
|
||||
{intl.formatMessage(intlMessages.dataSaving)}
|
||||
</div>
|
||||
<div className={styles.saving}>
|
||||
<label className={styles.label}>
|
||||
{intl.formatMessage(intlMessages.webcam)}
|
||||
</label>
|
||||
<Switch
|
||||
icons={false}
|
||||
defaultChecked={viewParticipantsWebcams}
|
||||
onChange={() => this.handleDataSavingChange('viewParticipantsWebcams')}
|
||||
ariaLabelledBy="webcam"
|
||||
ariaLabel={intl.formatMessage(intlMessages.webcam)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.saving}>
|
||||
<label className={styles.label}>
|
||||
{intl.formatMessage(intlMessages.screenshare)}
|
||||
</label>
|
||||
<Switch
|
||||
icons={false}
|
||||
defaultChecked={viewScreenshare}
|
||||
onChange={() => this.handleDataSavingChange('viewScreenshare')}
|
||||
ariaLabelledBy="screenshare"
|
||||
ariaLabel={intl.formatMessage(intlMessages.screenshare)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
closeModal,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
const isValidUrl = new RegExp(/^(http|https):\/\/[^ "]+$/).test(STATS.help);
|
||||
const { dataSaving } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
overlayClassName={styles.overlay}
|
||||
className={styles.modal}
|
||||
onRequestClose={closeModal}
|
||||
onRequestClose={() => closeModal(dataSaving, intl)}
|
||||
hideBorder
|
||||
contentLabel={intl.formatMessage(intlMessages.ariaTitle)}
|
||||
>
|
||||
@ -140,14 +214,15 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
{intl.formatMessage(intlMessages.description)}{' '}
|
||||
{isValidUrl
|
||||
{this.help
|
||||
&& (
|
||||
<a href={STATS.help} target="_blank" rel="noopener noreferrer">
|
||||
<a href={this.help} target="_blank" rel="noopener noreferrer">
|
||||
{`(${intl.formatMessage(intlMessages.more)})`}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{this.renderDataSaving()}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.wrapper}>
|
||||
{this.renderConnections()}
|
||||
|
@ -1,12 +1,18 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import ConnectionStatusService from '../service';
|
||||
import ConnectionStatusComponent from './component';
|
||||
|
||||
const connectionStatusContainer = props => <ConnectionStatusComponent {...props} />;
|
||||
|
||||
export default withModalMounter(withTracker(({ mountModal }) => ({
|
||||
closeModal: () => mountModal(null),
|
||||
closeModal: (dataSaving, intl) => {
|
||||
ConnectionStatusService.updateDataSavingSettings(dataSaving, intl);
|
||||
mountModal(null);
|
||||
},
|
||||
connectionStatus: ConnectionStatusService.getConnectionStatus(),
|
||||
dataSaving: _.clone(Settings.dataSaving),
|
||||
}))(connectionStatusContainer));
|
||||
|
@ -1,5 +1,13 @@
|
||||
@import '/imports/ui/stylesheets/mixins/focus';
|
||||
@import '/imports/ui/stylesheets/variables/_all';
|
||||
@import "/imports/ui/components/modal/simple/styles";
|
||||
|
||||
:root {
|
||||
--modal-margin: 3rem;
|
||||
--title-position-left: 2.2rem;
|
||||
--closeBtn-position-left: 2.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
left: var(--title-position-left);
|
||||
right: auto;
|
||||
@ -57,7 +65,7 @@
|
||||
.wrapper {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 24rem;
|
||||
max-height: 16rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
@ -101,10 +109,6 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.offline {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
@ -128,3 +132,18 @@
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.dataSaving {
|
||||
background-color: var(--color-off-white);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.saving {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
@ -1,33 +1,68 @@
|
||||
import { defineMessages } from 'react-intl';
|
||||
import ConnectionStatus from '/imports/api/connection-status';
|
||||
import Users from '/imports/api/users';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import Logger from '/imports/startup/client/logger';
|
||||
import _ from 'lodash';
|
||||
import { Session } from 'meteor/session';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
|
||||
const STATS = Meteor.settings.public.stats;
|
||||
const NOTIFICATION = STATS.notification;
|
||||
const STATS_LENGTH = STATS.length;
|
||||
const STATS_INTERVAL = STATS.interval;
|
||||
const STATS_LOG = STATS.log;
|
||||
const RTT_INTERVAL = STATS_LENGTH * STATS_INTERVAL;
|
||||
// Set a bottom threshold to avoid log flooding
|
||||
const RTT_LOG_THRESHOLD = STATS.rtt[STATS.level.indexOf('danger')];
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
let audioStats = '';
|
||||
const audioStatsDep = new Tracker.Dependency();
|
||||
const intlMessages = defineMessages({
|
||||
saved: {
|
||||
id: 'app.settings.save-notification.label',
|
||||
description: 'Label shown in toast when data savings are saved',
|
||||
},
|
||||
notification: {
|
||||
id: 'app.connection-status.notification',
|
||||
description: 'Label shown in toast when connection loss is detected',
|
||||
},
|
||||
});
|
||||
|
||||
let stats = -1;
|
||||
const statsDep = new Tracker.Dependency();
|
||||
|
||||
let statsTimeout = null;
|
||||
|
||||
const getHelp = () => STATS.help;
|
||||
const URL_REGEX = new RegExp(/^(http|https):\/\/[^ "]+$/);
|
||||
const getHelp = () => {
|
||||
if (URL_REGEX.test(STATS.help)) return STATS.help;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getLevel = () => STATS.level;
|
||||
|
||||
const getAudioStats = () => {
|
||||
audioStatsDep.depend();
|
||||
return audioStats;
|
||||
const getStats = () => {
|
||||
statsDep.depend();
|
||||
return STATS.level[stats];
|
||||
};
|
||||
|
||||
const setAudioStats = (level = '') => {
|
||||
if (audioStats !== level) {
|
||||
audioStats = level;
|
||||
audioStatsDep.changed();
|
||||
const setStats = (level = -1) => {
|
||||
if (stats !== level) {
|
||||
stats = level;
|
||||
statsDep.changed();
|
||||
addConnectionStatus(level);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStats = (level) => {
|
||||
if (level > stats) {
|
||||
setStats(level);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAudioStatsEvent = (event) => {
|
||||
const { detail } = event;
|
||||
if (detail) {
|
||||
@ -37,21 +72,64 @@ const handleAudioStatsEvent = (event) => {
|
||||
for (let i = STATS.level.length - 1; i >= 0; i--) {
|
||||
if (loss > STATS.loss[i] || jitter > STATS.jitter[i]) {
|
||||
active = true;
|
||||
setAudioStats(STATS.level[i]);
|
||||
handleStats(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (active) {
|
||||
if (statsTimeout !== null) clearTimeout(statsTimeout);
|
||||
statsTimeout = setTimeout(() => {
|
||||
setAudioStats();
|
||||
}, STATS.length * STATS.interval);
|
||||
}
|
||||
|
||||
if (active) startStatsTimeout();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSocketStatsEvent = (event) => {
|
||||
const { detail } = event;
|
||||
if (detail) {
|
||||
const { rtt } = detail;
|
||||
let active = false;
|
||||
// From higher to lower
|
||||
for (let i = STATS.level.length - 1; i >= 0; i--) {
|
||||
if (rtt > STATS.rtt[i]) {
|
||||
active = true;
|
||||
handleStats(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (active) startStatsTimeout();
|
||||
}
|
||||
};
|
||||
|
||||
const startStatsTimeout = () => {
|
||||
if (statsTimeout !== null) clearTimeout(statsTimeout);
|
||||
|
||||
statsTimeout = setTimeout(() => {
|
||||
setStats();
|
||||
}, STATS.timeout);
|
||||
};
|
||||
|
||||
const addConnectionStatus = (level) => {
|
||||
if (level !== '') makeCall('addConnectionStatus', level);
|
||||
if (level !== -1) makeCall('addConnectionStatus', STATS.level[level]);
|
||||
};
|
||||
|
||||
const fetchRoundTripTime = () => {
|
||||
const t0 = Date.now();
|
||||
makeCall('voidConnection').then(() => {
|
||||
const tf = Date.now();
|
||||
const rtt = tf - t0;
|
||||
|
||||
if (STATS_LOG && rtt > RTT_LOG_THRESHOLD) {
|
||||
Logger.info(
|
||||
{
|
||||
logCode: 'rtt',
|
||||
extraInfo: { rtt },
|
||||
},
|
||||
'Calculated round-trip time in milliseconds',
|
||||
);
|
||||
}
|
||||
|
||||
const event = new CustomEvent('socketstats', { detail: { rtt } });
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
};
|
||||
|
||||
const sortLevel = (a, b) => {
|
||||
@ -63,7 +141,51 @@ const sortLevel = (a, b) => {
|
||||
if (indexOfA > indexOfB) return -1;
|
||||
};
|
||||
|
||||
const getMyConnectionStatus = () => {
|
||||
const myConnectionStatus = ConnectionStatus.findOne(
|
||||
{
|
||||
meetingId: Auth.meetingID,
|
||||
userId: Auth.userID,
|
||||
},
|
||||
{ fields:
|
||||
{
|
||||
level: 1,
|
||||
timestamp: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const me = Users.findOne(
|
||||
{
|
||||
meetingId: Auth.meetingID,
|
||||
userId: Auth.userID,
|
||||
},
|
||||
{ fields:
|
||||
{
|
||||
avatar: 1,
|
||||
color: 1,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (myConnectionStatus) {
|
||||
return [{
|
||||
name: Auth.fullname,
|
||||
avatar: me.avatar,
|
||||
you: true,
|
||||
moderator: false,
|
||||
color: me.color,
|
||||
level: myConnectionStatus.level,
|
||||
timestamp: myConnectionStatus.timestamp,
|
||||
}];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const getConnectionStatus = () => {
|
||||
if (!isModerator()) return getMyConnectionStatus();
|
||||
|
||||
const connectionStatus = ConnectionStatus.find(
|
||||
{ meetingId: Auth.meetingID },
|
||||
).fetch().map(status => {
|
||||
@ -89,7 +211,6 @@ const getConnectionStatus = () => {
|
||||
role: 1,
|
||||
avatar: 1,
|
||||
color: 1,
|
||||
connectionStatus: 1,
|
||||
},
|
||||
},
|
||||
).fetch().reduce((result, user) => {
|
||||
@ -99,7 +220,6 @@ const getConnectionStatus = () => {
|
||||
role,
|
||||
avatar,
|
||||
color,
|
||||
connectionStatus: userStatus,
|
||||
} = user;
|
||||
|
||||
const status = connectionStatus.find(status => status.userId === userId);
|
||||
@ -108,7 +228,6 @@ const getConnectionStatus = () => {
|
||||
result.push({
|
||||
name,
|
||||
avatar,
|
||||
offline: userStatus === 'offline',
|
||||
you: Auth.userID === userId,
|
||||
moderator: role === ROLE_MODERATOR,
|
||||
color,
|
||||
@ -123,15 +242,81 @@ const getConnectionStatus = () => {
|
||||
|
||||
const isEnabled = () => STATS.enabled;
|
||||
|
||||
let roundTripTimeInterval = null;
|
||||
|
||||
const startRoundTripTime = () => {
|
||||
if (!isEnabled()) return;
|
||||
|
||||
stopRoundTripTime();
|
||||
|
||||
roundTripTimeInterval = setInterval(fetchRoundTripTime, RTT_INTERVAL);
|
||||
};
|
||||
|
||||
const stopRoundTripTime = () => {
|
||||
if (roundTripTimeInterval) {
|
||||
clearInterval(roundTripTimeInterval);
|
||||
}
|
||||
}
|
||||
|
||||
const isModerator = () => {
|
||||
const user = Users.findOne(
|
||||
{
|
||||
meetingId: Auth.meetingID,
|
||||
userId: Auth.userID,
|
||||
},
|
||||
{ fields: { role: 1 }},
|
||||
);
|
||||
|
||||
if (user && user.role === ROLE_MODERATOR) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (STATS.enabled) {
|
||||
window.addEventListener('audiostats', handleAudioStatsEvent);
|
||||
window.addEventListener('socketstats', handleSocketStatsEvent);
|
||||
}
|
||||
|
||||
const updateDataSavingSettings = (dataSaving, intl) => {
|
||||
if (!_.isEqual(Settings.dataSaving, dataSaving)) {
|
||||
Settings.dataSaving = dataSaving;
|
||||
Settings.save();
|
||||
if (intl) notify(intl.formatMessage(intlMessages.saved), 'info', 'settings');
|
||||
}
|
||||
};
|
||||
|
||||
const getNotified = () => {
|
||||
const notified = Session.get('connectionStatusNotified');
|
||||
|
||||
// Since notified can be undefined we need a boolean verification
|
||||
return notified === true;
|
||||
};
|
||||
|
||||
const notification = (level, intl) => {
|
||||
if (!NOTIFICATION[level]) return null;
|
||||
|
||||
// Avoid toast spamming
|
||||
const notified = getNotified();
|
||||
if (notified) {
|
||||
return null;
|
||||
} else {
|
||||
Session.set('connectionStatusNotified', true);
|
||||
}
|
||||
|
||||
if (intl) notify(intl.formatMessage(intlMessages.notification), level, 'network');
|
||||
};
|
||||
|
||||
export default {
|
||||
addConnectionStatus,
|
||||
getConnectionStatus,
|
||||
getAudioStats,
|
||||
getStats,
|
||||
getHelp,
|
||||
getLevel,
|
||||
isEnabled,
|
||||
notification,
|
||||
startRoundTripTime,
|
||||
stopRoundTripTime,
|
||||
updateDataSavingSettings,
|
||||
};
|
||||
|
@ -16,6 +16,8 @@ import DefaultContent from '../presentation/default-content/component';
|
||||
import ExternalVideoContainer from '../external-video-player/container';
|
||||
import Storage from '../../services/storage/session';
|
||||
import { withLayoutConsumer } from '/imports/ui/components/layout/context';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import breakoutService from '/imports/ui/components/breakout-room/service';
|
||||
|
||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||
const KURENTO_CONFIG = Meteor.settings.public.kurento;
|
||||
@ -106,6 +108,8 @@ class MediaContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
let userWasInBreakout = false;
|
||||
|
||||
export default withLayoutConsumer(withModalMounter(withTracker(() => {
|
||||
const { dataSaving } = Settings;
|
||||
const { viewParticipantsWebcams, viewScreenshare } = dataSaving;
|
||||
@ -127,6 +131,31 @@ export default withLayoutConsumer(withModalMounter(withTracker(() => {
|
||||
data.children = <ScreenshareContainer />;
|
||||
}
|
||||
|
||||
const userIsInBreakout = breakoutService.getBreakoutUserIsIn(Auth.userID);
|
||||
let deviceIds = Session.get('deviceIds');
|
||||
|
||||
if (!userIsInBreakout && userWasInBreakout && deviceIds && deviceIds !== '') {
|
||||
/* used when re-sharing cameras after leaving a breakout room.
|
||||
it is needed in cases where the user has more than one active camera
|
||||
so we only share the second camera after the first
|
||||
has finished loading (can't share more than one at the same time) */
|
||||
const canConnect = Session.get('canConnect');
|
||||
|
||||
deviceIds = deviceIds.split(',');
|
||||
|
||||
if (canConnect) {
|
||||
const deviceId = deviceIds.shift();
|
||||
|
||||
Session.set('canConnect', false);
|
||||
Session.set('WebcamDeviceId', deviceId);
|
||||
Session.set('deviceIds', deviceIds.join(','));
|
||||
|
||||
VideoService.joinVideo(deviceId);
|
||||
}
|
||||
} else {
|
||||
userWasInBreakout = userIsInBreakout;
|
||||
}
|
||||
|
||||
const { streams: usersVideo } = VideoService.getVideoStreams();
|
||||
data.usersVideo = usersVideo;
|
||||
|
||||
|
@ -11,6 +11,8 @@ import { styles } from './styles.scss';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import RecordingIndicator from './recording-indicator/container';
|
||||
import TalkingIndicatorContainer from '/imports/ui/components/nav-bar/talking-indicator/container';
|
||||
import ConnectionStatusButton from '/imports/ui/components/connection-status/button/container';
|
||||
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
|
||||
import SettingsDropdownContainer from './settings-dropdown/container';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -125,6 +127,7 @@ class NavBar extends Component {
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
{ConnectionStatusService.isEnabled() ? <ConnectionStatusButton /> : null}
|
||||
<SettingsDropdownContainer amIModerator={amIModerator} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,7 +8,6 @@ import Meetings, { MeetingTimeRemaining } from '/imports/api/meetings';
|
||||
import Users from '/imports/api/users';
|
||||
import BreakoutRemainingTime from '/imports/ui/components/breakout-room/breakout-remaining-time/container';
|
||||
import SlowConnection from '/imports/ui/components/slow-connection/component';
|
||||
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
|
||||
import { styles } from './styles.scss';
|
||||
|
||||
import breakoutService from '/imports/ui/components/breakout-room/service';
|
||||
@ -150,22 +149,6 @@ export default injectIntl(withTracker(({ intl }) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (ConnectionStatusService.isEnabled()) {
|
||||
const stats = ConnectionStatusService.getAudioStats();
|
||||
if (stats) {
|
||||
if (ConnectionStatusService.getLevel().includes(stats)) {
|
||||
data.message = (
|
||||
<SlowConnection effectiveConnectionType={stats}>
|
||||
{intl.formatMessage(intlMessages.slowEffectiveConnectionDetected)}{' '}
|
||||
<a href={ConnectionStatusService.getHelp()} target="_blank" rel="noopener noreferrer">
|
||||
{intl.formatMessage(intlMessages.slowEffectiveConnectionHelpLink)}
|
||||
</a>
|
||||
</SlowConnection>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
data.color = 'primary';
|
||||
switch (status) {
|
||||
|
@ -12,15 +12,13 @@ const intlMessages = defineMessages({
|
||||
|
||||
const ClosePresentationComponent = ({ intl, toggleSwapLayout }) => (
|
||||
<Button
|
||||
role="button"
|
||||
aria-labelledby="closeLabel"
|
||||
aria-describedby="closeDesc"
|
||||
color="primary"
|
||||
icon="minus"
|
||||
size="sm"
|
||||
data-test="hidePresentationButton"
|
||||
onClick={toggleSwapLayout}
|
||||
label={intl.formatMessage(intlMessages.closePresentationLabel)}
|
||||
aria-label={intl.formatMessage(intlMessages.closePresentationLabel)}
|
||||
hideLabel
|
||||
className={styles.button}
|
||||
/>
|
||||
|
@ -36,6 +36,8 @@ class Subscriptions extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
let usersPersistentDataHandler = null;
|
||||
|
||||
export default withTracker(() => {
|
||||
const { credentials } = Auth;
|
||||
const { meetingId, requesterUserId } = credentials;
|
||||
@ -105,6 +107,9 @@ export default withTracker(() => {
|
||||
const chatIds = chats.map(chat => chat.chatId);
|
||||
groupChatMessageHandler = Meteor.subscribe('group-chat-msg', chatIds, subscriptionErrorHandler);
|
||||
}
|
||||
if (ready && !usersPersistentDataHandler) {
|
||||
usersPersistentDataHandler = Meteor.subscribe('users-persistent-data');
|
||||
}
|
||||
|
||||
return {
|
||||
subscriptionsReady: ready,
|
||||
|
@ -3,10 +3,18 @@ import { withTracker } from 'meteor/react-meteor-data';
|
||||
import UserListService from '/imports/ui/components/user-list/service';
|
||||
import UserParticipants from './component';
|
||||
import { meetingIsBreakout } from '/imports/ui/components/app/service';
|
||||
import ChatService from '/imports/ui/components/chat/service';
|
||||
|
||||
const UserParticipantsContainer = props => <UserParticipants {...props} />;
|
||||
|
||||
export default withTracker(() => ({
|
||||
export default withTracker(() => {
|
||||
ChatService.removePackagedClassAttribute(
|
||||
['ReactVirtualized__Grid', 'ReactVirtualized__Grid__innerScrollContainer'],
|
||||
'role',
|
||||
);
|
||||
|
||||
return ({
|
||||
users: UserListService.getUsers(),
|
||||
meetingIsBreakout: meetingIsBreakout(),
|
||||
}))(UserParticipantsContainer);
|
||||
});
|
||||
})(UserParticipantsContainer);
|
||||
|
@ -10,11 +10,9 @@ import DropdownContent from '/imports/ui/components/dropdown/content/component';
|
||||
import DropdownList from '/imports/ui/components/dropdown/list/component';
|
||||
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
|
||||
import LockViewersContainer from '/imports/ui/components/lock-viewers/container';
|
||||
import ConnectionStatusContainer from '/imports/ui/components/connection-status/modal/container';
|
||||
import GuestPolicyContainer from '/imports/ui/components/waiting-users/guest-policy/container';
|
||||
import BreakoutRoom from '/imports/ui/components/actions-bar/create-breakout-room/container';
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
|
||||
import CaptionsWriterMenu from '/imports/ui/components/captions/writer-menu/container';
|
||||
import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
|
||||
import { styles } from './styles';
|
||||
@ -74,14 +72,6 @@ const intlMessages = defineMessages({
|
||||
id: 'app.userList.userOptions.lockViewersDesc',
|
||||
description: 'Lock viewers description',
|
||||
},
|
||||
connectionStatusLabel: {
|
||||
id: 'app.userList.userOptions.connectionStatusLabel',
|
||||
description: 'Connection status label',
|
||||
},
|
||||
connectionStatusDesc: {
|
||||
id: 'app.userList.userOptions.connectionStatusDesc',
|
||||
description: 'Connection status description',
|
||||
},
|
||||
guestPolicyLabel: {
|
||||
id: 'app.userList.userOptions.guestPolicyLabel',
|
||||
description: 'Guest policy label',
|
||||
@ -148,7 +138,6 @@ class UserOptions extends PureComponent {
|
||||
this.muteId = _.uniqueId('list-item-');
|
||||
this.muteAllId = _.uniqueId('list-item-');
|
||||
this.lockId = _.uniqueId('list-item-');
|
||||
this.connectionStatusId = _.uniqueId('list-item-');
|
||||
this.guestPolicyId = _.uniqueId('list-item-');
|
||||
this.createBreakoutId = _.uniqueId('list-item-');
|
||||
this.saveUsersNameId = _.uniqueId('list-item-');
|
||||
@ -298,15 +287,6 @@ class UserOptions extends PureComponent {
|
||||
onClick={() => mountModal(<LockViewersContainer />)}
|
||||
/>) : null
|
||||
),
|
||||
(ConnectionStatusService.isEnabled() && isMeteorConnected ? (
|
||||
<DropdownListItem
|
||||
key={this.connectionStatusId}
|
||||
icon="warning"
|
||||
label={intl.formatMessage(intlMessages.connectionStatusLabel)}
|
||||
description={intl.formatMessage(intlMessages.connectionStatusDesc)}
|
||||
onClick={() => mountModal(<ConnectionStatusContainer />)}
|
||||
/>) : null
|
||||
),
|
||||
(!meetingIsBreakout && isMeteorConnected ? (
|
||||
<DropdownListItem
|
||||
key={this.guestPolicyId}
|
||||
|
@ -142,6 +142,22 @@ class VideoService {
|
||||
this.isConnected = true;
|
||||
}
|
||||
|
||||
storeDeviceIds() {
|
||||
const streams = VideoStreams.find(
|
||||
{
|
||||
meetingId: Auth.meetingID,
|
||||
userId: Auth.userID,
|
||||
}, { fields: { deviceId: 1 } },
|
||||
).fetch();
|
||||
|
||||
let deviceIds = [];
|
||||
streams.forEach(s => {
|
||||
deviceIds.push(s.deviceId);
|
||||
}
|
||||
);
|
||||
Session.set('deviceIds', deviceIds.join());
|
||||
}
|
||||
|
||||
exitVideo() {
|
||||
if (this.isConnected) {
|
||||
logger.info({
|
||||
@ -801,6 +817,7 @@ class VideoService {
|
||||
const videoService = new VideoService();
|
||||
|
||||
export default {
|
||||
storeDeviceIds: () => videoService.storeDeviceIds(),
|
||||
exitVideo: () => videoService.exitVideo(),
|
||||
joinVideo: deviceId => videoService.joinVideo(deviceId),
|
||||
stopVideo: cameraId => videoService.stopVideo(cameraId),
|
||||
|
@ -122,6 +122,12 @@ class VideoListItem extends Component {
|
||||
const { videoIsReady } = this.state;
|
||||
if (!videoIsReady) this.setState({ videoIsReady: true });
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
/* used when re-sharing cameras after leaving a breakout room.
|
||||
it is needed in cases where the user has more than one active camera
|
||||
so we only share the second camera after the first
|
||||
has finished loading (can't share more than one at the same time) */
|
||||
Session.set('canConnect', true);
|
||||
}
|
||||
|
||||
getAvailableActions() {
|
||||
|
@ -470,26 +470,21 @@ class PollDrawComponent extends Component {
|
||||
fill={backgroundColor}
|
||||
strokeWidth={thickness}
|
||||
/>
|
||||
<text
|
||||
x={innerRect.x}
|
||||
y={innerRect.y}
|
||||
fill="#333333"
|
||||
fontFamily="Arial"
|
||||
fontSize={calcFontSize}
|
||||
textAnchor={isRTL ? 'end' : 'start'}
|
||||
>
|
||||
{extendedTextArray.map(line => (
|
||||
<tspan
|
||||
<text
|
||||
x={line.keyColumn.xLeft}
|
||||
y={line.keyColumn.yLeft}
|
||||
dy={maxLineHeight / 2}
|
||||
key={`${line.key}_key`}
|
||||
fill="#333333"
|
||||
fontFamily="Arial"
|
||||
fontSize={calcFontSize}
|
||||
textAnchor={isRTL ? 'end' : 'start'}
|
||||
className={styles.outline}
|
||||
>
|
||||
{line.keyColumn.keyString}
|
||||
</tspan>
|
||||
))}
|
||||
</text>
|
||||
))}
|
||||
{extendedTextArray.map(line => (
|
||||
<rect
|
||||
key={`${line.key}_bar`}
|
||||
|
@ -18,6 +18,7 @@
|
||||
--color-success: #008081;
|
||||
--color-danger: #DF2721;
|
||||
--color-warning: purple;
|
||||
--color-offline: var(--color-gray-light);
|
||||
|
||||
--color-background: var(--color-gray-dark);
|
||||
|
||||
|
@ -159,7 +159,7 @@ const calculateInterval = (stats) => {
|
||||
bytes: {
|
||||
received: diff(single, first.bytes.received, last.bytes.received)
|
||||
},
|
||||
jitter: single ? first.jitter : last.jitter
|
||||
jitter: Math.max.apply(Math, stats.map(s => s.jitter))
|
||||
};
|
||||
};
|
||||
|
||||
@ -190,7 +190,7 @@ const logResult = (id, result) => {
|
||||
if (!iteration || iteration % STATS_LENGTH !== 0) return null;
|
||||
|
||||
const duration = STATS_LENGTH * STATS_INTERVAL / 1000;
|
||||
logger.info(
|
||||
logger.debug(
|
||||
{
|
||||
logCode: 'stats_monitor_result',
|
||||
extraInfo: {
|
||||
|
@ -163,10 +163,10 @@ public:
|
||||
mediaTimeouts:
|
||||
maxConnectionAttempts: 2
|
||||
# Base screen media timeout (send|recv)
|
||||
baseTimeout: 15000
|
||||
baseTimeout: 30000
|
||||
# Max timeout: used as the max camera subscribe reconnection timeout. Each
|
||||
# subscribe reattempt increases the reconnection timer up to this
|
||||
maxTimeout: 35000
|
||||
maxTimeout: 60000
|
||||
timeoutIncreaseFactor: 1.5
|
||||
constraints:
|
||||
video:
|
||||
@ -420,7 +420,11 @@ public:
|
||||
enabled: true
|
||||
interval: 2000
|
||||
length: 5
|
||||
timeout: 30000
|
||||
log: false
|
||||
notification:
|
||||
warning: false
|
||||
error: true
|
||||
jitter:
|
||||
- 10
|
||||
- 20
|
||||
@ -429,6 +433,10 @@ public:
|
||||
- 0.05
|
||||
- 0.1
|
||||
- 0.2
|
||||
rtt:
|
||||
- 500
|
||||
- 1000
|
||||
- 2000
|
||||
level:
|
||||
- warning
|
||||
- danger
|
||||
|
@ -277,17 +277,6 @@ function WebRtcPeer(mode, options, callback) {
|
||||
callback(null, localDescription.sdp, self.processAnswer.bind(self));
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -305,10 +294,6 @@ function WebRtcPeer(mode, options, callback) {
|
||||
offer = mangleSdpToAddSimulcast(offer);
|
||||
return pc.setLocalDescription(offer);
|
||||
}).then(() => {
|
||||
// The Safari offer release was already binded to the gathering state
|
||||
if (isSafari) {
|
||||
return;
|
||||
}
|
||||
descriptionCallback();
|
||||
}).catch(callback);
|
||||
};
|
||||
|
@ -104,8 +104,6 @@
|
||||
"app.userList.userOptions.lockViewersDesc": "Lock certain functionalities for attendees of the meeting",
|
||||
"app.userList.userOptions.guestPolicyLabel": "Guest policy",
|
||||
"app.userList.userOptions.guestPolicyDesc": "Change meeting guest policy setting",
|
||||
"app.userList.userOptions.connectionStatusLabel": "Connection status",
|
||||
"app.userList.userOptions.connectionStatusDesc": "View users' connection status",
|
||||
"app.userList.userOptions.disableCam": "Viewers' webcams are disabled",
|
||||
"app.userList.userOptions.disableMic": "Viewers' microphones are disabled",
|
||||
"app.userList.userOptions.disablePrivChat": "Private chat is disabled",
|
||||
@ -618,7 +616,8 @@
|
||||
"app.connection-status.description": "View users' connection status",
|
||||
"app.connection-status.empty": "There are currently no reported connection issues",
|
||||
"app.connection-status.more": "more",
|
||||
"app.connection-status.offline": "offline",
|
||||
"app.connection-status.label": "Connection status",
|
||||
"app.connection-status.notification": "Loss in your connection was detected",
|
||||
"app.recording.startTitle": "Start recording",
|
||||
"app.recording.stopTitle": "Pause recording",
|
||||
"app.recording.resumeTitle": "Resume recording",
|
||||
|
@ -21,6 +21,7 @@ import '/imports/api/whiteboard-multi-user/server';
|
||||
import '/imports/api/video-streams/server';
|
||||
import '/imports/api/network-information/server';
|
||||
import '/imports/api/users-infos/server';
|
||||
import '/imports/api/users-persistent-data/server';
|
||||
import '/imports/api/connection-status/server';
|
||||
import '/imports/api/note/server';
|
||||
import '/imports/api/external-videos/server';
|
||||
|
@ -27,6 +27,7 @@ module BigBlueButton
|
||||
$bbb_props = YAML::load(File.open(File.expand_path('../../../scripts/bigbluebutton.yml', __FILE__)))
|
||||
$recording_dir = $bbb_props['recording_dir']
|
||||
$raw_recording_dir = "#{$recording_dir}/raw"
|
||||
$store_recording_status = $bbb_props['store_recording_status']
|
||||
|
||||
# Class to wrap Redis so we can mock
|
||||
# for testing
|
||||
@ -135,6 +136,7 @@ module BigBlueButton
|
||||
end
|
||||
|
||||
RECORDINGS_CHANNEL = "bigbluebutton:from-rap"
|
||||
RAP_STATUS_LIST = "bigbluebutton:rap:status"
|
||||
|
||||
def put_message(message_type, meeting_id, additional_payload = {})
|
||||
events_xml = "#{$raw_recording_dir}/#{meeting_id}/events.xml"
|
||||
@ -149,6 +151,11 @@ module BigBlueButton
|
||||
"meeting_id" => meeting_id
|
||||
})
|
||||
@redis.publish RECORDINGS_CHANNEL, msg.to_json
|
||||
|
||||
if $store_recording_status
|
||||
@redis.lpush RAP_STATUS_LIST, msg.to_json
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def put_message_workflow(message_type, workflow, meeting_id, additional_payload = {})
|
||||
@ -157,6 +164,10 @@ module BigBlueButton
|
||||
})
|
||||
end
|
||||
|
||||
def put_archive_norecord(meeting_id, additional_payload = {})
|
||||
put_message "archive_norecord", meeting_id, additional_payload
|
||||
end
|
||||
|
||||
def put_archive_started(meeting_id, additional_payload = {})
|
||||
put_message "archive_started", meeting_id, additional_payload
|
||||
end
|
||||
|
4
record-and-playback/core/lib/recordandplayback/workers/archive_worker.rb
Normal file → Executable file
4
record-and-playback/core/lib/recordandplayback/workers/archive_worker.rb
Normal file → Executable file
@ -42,6 +42,10 @@ module BigBlueButton
|
||||
!File.exist?(@archived_fail)
|
||||
)
|
||||
|
||||
if File.exist?(@archived_norecord)
|
||||
@publisher.put_archive_norecord(@meeting_id)
|
||||
end
|
||||
|
||||
@publisher.put_archive_ended(@meeting_id, success: step_succeeded, step_time: step_time)
|
||||
|
||||
if step_succeeded
|
||||
|
0
record-and-playback/core/lib/recordandplayback/workers/process_worker.rb
Normal file → Executable file
0
record-and-playback/core/lib/recordandplayback/workers/process_worker.rb
Normal file → Executable file
1
record-and-playback/core/lib/recordandplayback/workers/publish_worker.rb
Normal file → Executable file
1
record-and-playback/core/lib/recordandplayback/workers/publish_worker.rb
Normal file → Executable file
@ -35,6 +35,7 @@ module BigBlueButton
|
||||
|
||||
# If the publish directory exists, the script does nothing
|
||||
FileUtils.rm_rf("#{@recording_dir}/publish/#{@format_name}/#{@full_id}")
|
||||
|
||||
remove_status_files
|
||||
|
||||
# For legacy reasons, the meeting ID passed to the publish script contains
|
||||
|
0
record-and-playback/core/lib/recordandplayback/workers/sanity_worker.rb
Normal file → Executable file
0
record-and-playback/core/lib/recordandplayback/workers/sanity_worker.rb
Normal file → Executable file
@ -22,6 +22,12 @@ redis_port: 6379
|
||||
# redis_workers_host: 127.0.0.1
|
||||
# redis_workers_port: 6379
|
||||
|
||||
# Set to true to insert recording process status into
|
||||
# redis list with key "store_recording_status: true".
|
||||
# This is useful if you want to track progress status
|
||||
# and have another script process it.
|
||||
store_recording_status: false
|
||||
|
||||
# Sequence of recording steps. Keys are the current step, values
|
||||
# are the next step(s). Examples:
|
||||
# current_step: next_step
|
||||
|
Loading…
Reference in New Issue
Block a user