diff --git a/bbb-learning-dashboard/src/index.css b/bbb-learning-dashboard/src/index.css
index a3319c71b3..4949bc1fe1 100644
--- a/bbb-learning-dashboard/src/index.css
+++ b/bbb-learning-dashboard/src/index.css
@@ -1,4 +1,34 @@
/* ./src/index.css */
@tailwind base;
@tailwind components;
-@tailwind utilities;
\ No newline at end of file
+@tailwind utilities;
+
+@layer utilities {
+ .bg-inherit {
+ background-color: inherit;
+ }
+}
+
+.translate-x-0 {
+ --tw-translate-x: 0px;
+}
+
+.translate-x-2 {
+ --tw-translate-x: 0.5rem;
+}
+
+.translate-x-4 {
+ --tw-translate-x: 1rem;
+}
+
+[dir="rtl"] .translate-x-0 {
+ --tw-translate-x: 0px;
+}
+
+[dir="rtl"] .translate-x-2 {
+ --tw-translate-x: -0.5rem;
+}
+
+[dir="rtl"] .translate-x-4 {
+ --tw-translate-x: -1rem;
+}
diff --git a/bbb-learning-dashboard/src/services/EmojiService.js b/bbb-learning-dashboard/src/services/EmojiService.js
index b64fdc98ff..20fdd737ce 100644
--- a/bbb-learning-dashboard/src/services/EmojiService.js
+++ b/bbb-learning-dashboard/src/services/EmojiService.js
@@ -60,3 +60,15 @@ export function getUserEmojisSummary(user, skipNames = null, start = null, end =
});
return userEmojis;
}
+
+export function filterUserEmojis(user, skipNames = null, start = null, end = null) {
+ const userEmojis = [];
+ user.emojis.forEach((emoji) => {
+ if (typeof emojiConfigs[emoji.name] === 'undefined') return;
+ if (skipNames != null && skipNames.split(',').indexOf(emoji.name) > -1) return;
+ if (start != null && emoji.sentOn < start) return;
+ if (end != null && emoji.sentOn > end) return;
+ userEmojis.push(emoji);
+ });
+ return userEmojis;
+}
diff --git a/bbb-learning-dashboard/src/services/UserService.js b/bbb-learning-dashboard/src/services/UserService.js
new file mode 100644
index 0000000000..84dc8a4c94
--- /dev/null
+++ b/bbb-learning-dashboard/src/services/UserService.js
@@ -0,0 +1,202 @@
+import { emojiConfigs, filterUserEmojis } from './EmojiService';
+
+export function getActivityScore(user, allUsers, totalOfPolls) {
+ if (user.isModerator) return 0;
+
+ const allUsersArr = Object.values(allUsers || {}).filter((currUser) => !currUser.isModerator);
+ let userPoints = 0;
+
+ // Calculate points of Talking
+ const usersTalkTime = allUsersArr.map((currUser) => currUser.talk.totalTime);
+ const maxTalkTime = Math.max(...usersTalkTime);
+ if (maxTalkTime > 0) {
+ userPoints += (user.talk.totalTime / maxTalkTime) * 2;
+ }
+
+ // Calculate points of Chatting
+ const usersTotalOfMessages = allUsersArr.map((currUser) => currUser.totalOfMessages);
+ const maxMessages = Math.max(...usersTotalOfMessages);
+ if (maxMessages > 0) {
+ userPoints += (user.totalOfMessages / maxMessages) * 2;
+ }
+
+ // Calculate points of Raise hand
+ const usersRaiseHand = allUsersArr.map((currUser) => currUser.emojis.filter((emoji) => emoji.name === 'raiseHand').length);
+ const maxRaiseHand = Math.max(...usersRaiseHand);
+ const userRaiseHand = user.emojis.filter((emoji) => emoji.name === 'raiseHand').length;
+ if (maxRaiseHand > 0) {
+ userPoints += (userRaiseHand / maxRaiseHand) * 2;
+ }
+
+ // Calculate points of Emojis
+ const usersEmojis = allUsersArr.map((currUser) => currUser.emojis.filter((emoji) => emoji.name !== 'raiseHand').length);
+ const maxEmojis = Math.max(...usersEmojis);
+ const userEmojis = user.emojis.filter((emoji) => emoji.name !== 'raiseHand').length;
+ if (maxEmojis > 0) {
+ userPoints += (userEmojis / maxEmojis) * 2;
+ }
+
+ // Calculate points of Polls
+ if (totalOfPolls > 0) {
+ userPoints += (Object.values(user.answers || {}).length / totalOfPolls) * 2;
+ }
+
+ return userPoints;
+}
+
+export function getSumOfTime(eventsArr) {
+ return eventsArr.reduce((prevVal, elem) => {
+ if (elem.stoppedOn > 0) return prevVal + (elem.stoppedOn - elem.startedOn);
+ return prevVal + (new Date().getTime() - elem.startedOn);
+ }, 0);
+}
+
+export function tsToHHmmss(ts) {
+ return (new Date(ts).toISOString().substr(11, 8));
+}
+
+const tableHeaderFields = [
+ {
+ id: 'name',
+ defaultMessage: 'Name',
+ },
+ {
+ id: 'moderator',
+ defaultMessage: 'Moderator',
+ },
+ {
+ id: 'activityScore',
+ defaultMessage: 'Activity Score',
+ },
+ {
+ id: 'colTalk',
+ defaultMessage: 'Talk Time',
+ },
+ {
+ id: 'colWebcam',
+ defaultMessage: 'Webcam Time',
+ },
+ {
+ id: 'colMessages',
+ defaultMessage: 'Messages',
+ },
+ {
+ id: 'colEmojis',
+ defaultMessage: 'Emojis',
+ },
+ {
+ id: 'pollVotes',
+ defaultMessage: 'Poll Votes',
+ },
+ {
+ id: 'colRaiseHands',
+ defaultMessage: 'Raise Hands',
+ },
+ {
+ id: 'join',
+ defaultMessage: 'Join',
+ },
+ {
+ id: 'left',
+ defaultMessage: 'Left',
+ },
+ {
+ id: 'duration',
+ defaultMessage: 'Duration',
+ },
+];
+
+export function makeUserCSVData(users, polls, intl) {
+ const userRecords = {};
+ const userValues = Object.values(users || {});
+ const pollValues = Object.values(polls || {});
+ const skipEmojis = Object
+ .keys(emojiConfigs)
+ .filter((emoji) => emoji !== 'raiseHand')
+ .join(',');
+
+ for (let i = 0; i < userValues.length; i += 1) {
+ const user = userValues[i];
+ const webcam = getSumOfTime(user.webcams);
+ const duration = user.leftOn > 0
+ ? user.leftOn - user.registeredOn
+ : (new Date()).getTime() - user.registeredOn;
+
+ const userData = {
+ name: user.name,
+ moderator: user.isModerator.toString().toUpperCase(),
+ activityScore: intl.formatNumber(
+ getActivityScore(user, userValues, Object.values(polls || {}).length),
+ {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 1,
+ },
+ ),
+ talk: user.talk.totalTime > 0 ? tsToHHmmss(user.talk.totalTime) : '-',
+ webcam: webcam > 0 ? tsToHHmmss(webcam) : '-',
+ messages: user.totalOfMessages,
+ raiseHand: filterUserEmojis(user, 'raiseHand').length,
+ answers: Object.keys(user.answers).length,
+ emojis: filterUserEmojis(user, skipEmojis).length,
+ registeredOn: intl.formatDate(user.registeredOn, {
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ }),
+ leftOn: intl.formatDate(user.leftOn, {
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ }),
+ duration: tsToHHmmss(duration),
+ };
+
+ for (let j = 0; j < pollValues.length; j += 1) {
+ userData[`Poll_${j}`] = user.answers[pollValues[j].pollId] || '-';
+ }
+
+ const userFields = Object
+ .values(userData)
+ .map((data) => `"${data}"`);
+
+ userRecords[user.intId] = userFields.join(',');
+ }
+
+ const tableHeaderFieldsTranslated = tableHeaderFields
+ .map(({ id, defaultMessage }) => intl.formatMessage({
+ id: `app.learningDashboard.usersTable.${id}`,
+ defaultMessage,
+ }));
+
+ let header = tableHeaderFieldsTranslated.join(',');
+ let anonymousRecord = `"${intl.formatMessage({
+ id: 'app.learningDashboard.pollsTable.anonymousRowName',
+ defaultMessage: 'Anonymous',
+ })}"`;
+
+ // Skip the fields for the anonymous record
+ for (let k = 0; k < header.split(',').length - 1; k += 1) {
+ // Empty fields
+ anonymousRecord += ',""';
+ }
+
+ for (let i = 0; i < pollValues.length; i += 1) {
+ // Add the poll question headers
+ header += `,${pollValues[i].question || `Poll ${i + 1}`}`;
+
+ // Add the anonymous answers
+ anonymousRecord += `,"${pollValues[i].anonymousAnswers.join('\r\n')}"`;
+ }
+ userRecords.Anonymous = anonymousRecord;
+
+ return [
+ header,
+ Object.values(userRecords).join('\r\n'),
+ ].join('\r\n');
+}
diff --git a/bbb-learning-dashboard/tailwind.config.js b/bbb-learning-dashboard/tailwind.config.js
index 7b39079d2d..664d9719fa 100644
--- a/bbb-learning-dashboard/tailwind.config.js
+++ b/bbb-learning-dashboard/tailwind.config.js
@@ -8,4 +8,4 @@ module.exports = {
extend: {},
},
plugins: [],
-}
+};
diff --git a/bigbluebutton-html5/client/collection-mirror-initializer.js b/bigbluebutton-html5/client/collection-mirror-initializer.js
index 232381e665..f86765b5d0 100644
--- a/bigbluebutton-html5/client/collection-mirror-initializer.js
+++ b/bigbluebutton-html5/client/collection-mirror-initializer.js
@@ -19,6 +19,7 @@ import Captions from '/imports/api/captions';
import AuthTokenValidation from '/imports/api/auth-token-validation';
import Annotations from '/imports/api/annotations';
import Breakouts from '/imports/api/breakouts';
+import BreakoutsHistory from '/imports/api/breakouts-history';
import guestUsers from '/imports/api/guest-users';
import Meetings, { RecordMeetings, ExternalVideoMeetings, MeetingTimeRemaining } from '/imports/api/meetings';
import { UsersTyping } from '/imports/api/group-chat-msg';
@@ -52,6 +53,7 @@ export const localExternalVideoMeetingsSync = new AbstractCollection(ExternalVid
export const localMeetingTimeRemainingSync = new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining);
export const localUsersTypingSync = new AbstractCollection(UsersTyping, UsersTyping);
export const localBreakoutsSync = new AbstractCollection(Breakouts, Breakouts);
+export const localBreakoutsHistorySync = new AbstractCollection(BreakoutsHistory, BreakoutsHistory);
export const localGuestUsersSync = new AbstractCollection(guestUsers, guestUsers);
export const localMeetingsSync = new AbstractCollection(Meetings, Meetings);
export const localUsersSync = new AbstractCollection(Users, Users);
@@ -83,6 +85,7 @@ const collectionMirrorInitializer = () => {
localMeetingTimeRemainingSync.setupListeners();
localUsersTypingSync.setupListeners();
localBreakoutsSync.setupListeners();
+ localBreakoutsHistorySync.setupListeners();
localGuestUsersSync.setupListeners();
localMeetingsSync.setupListeners();
localUsersSync.setupListeners();
diff --git a/bigbluebutton-html5/imports/api/breakouts-history/index.js b/bigbluebutton-html5/imports/api/breakouts-history/index.js
new file mode 100644
index 0000000000..d9a7c17eea
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/breakouts-history/index.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+
+const collectionOptions = Meteor.isClient ? {
+ connection: null,
+} : {};
+
+const BreakoutsHistory = new Mongo.Collection('breakouts-history', collectionOptions);
+
+if (Meteor.isServer) {
+ BreakoutsHistory._ensureIndex({ meetingId: 1 });
+}
+
+export default BreakoutsHistory;
diff --git a/bigbluebutton-html5/imports/api/breakouts-history/server/eventHandlers.js b/bigbluebutton-html5/imports/api/breakouts-history/server/eventHandlers.js
new file mode 100644
index 0000000000..9418f5f1fe
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/breakouts-history/server/eventHandlers.js
@@ -0,0 +1,4 @@
+import RedisPubSub from '/imports/startup/server/redis';
+import handleBreakoutRoomsList from './handlers/breakoutRoomsList';
+
+RedisPubSub.on('BreakoutRoomsListEvtMsg', handleBreakoutRoomsList);
diff --git a/bigbluebutton-html5/imports/api/breakouts-history/server/handlers/breakoutRoomsList.js b/bigbluebutton-html5/imports/api/breakouts-history/server/handlers/breakoutRoomsList.js
new file mode 100644
index 0000000000..0b26a79310
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/breakouts-history/server/handlers/breakoutRoomsList.js
@@ -0,0 +1,35 @@
+import { check } from 'meteor/check';
+import BreakoutsHistory from '/imports/api/breakouts-history';
+import Logger from '/imports/startup/server/logger';
+
+export default function handleBreakoutRoomsList({ body }) {
+ const {
+ meetingId,
+ rooms,
+ } = body;
+
+ check(meetingId, String);
+
+ const selector = {
+ meetingId,
+ };
+
+ const modifier = {
+ $set: {
+ meetingId,
+ rooms,
+ },
+ };
+
+ try {
+ const { insertedId } = BreakoutsHistory.upsert(selector, modifier);
+
+ if (insertedId) {
+ Logger.info(`Added rooms to breakout-history Data: meeting=${meetingId}`);
+ } else {
+ Logger.info(`Upserted rooms to breakout-history Data: meeting=${meetingId}`);
+ }
+ } catch (err) {
+ Logger.error(`Adding note to the collection breakout-history: ${err}`);
+ }
+}
diff --git a/bigbluebutton-html5/imports/api/breakouts-history/server/index.js b/bigbluebutton-html5/imports/api/breakouts-history/server/index.js
new file mode 100644
index 0000000000..f993f38e5b
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/breakouts-history/server/index.js
@@ -0,0 +1,2 @@
+import './eventHandlers';
+import './publishers';
diff --git a/bigbluebutton-html5/imports/api/breakouts-history/server/publishers.js b/bigbluebutton-html5/imports/api/breakouts-history/server/publishers.js
new file mode 100644
index 0000000000..124d575d7c
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/breakouts-history/server/publishers.js
@@ -0,0 +1,56 @@
+import BreakoutsHistory from '/imports/api/breakouts-history';
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
+import Logger from '/imports/startup/server/logger';
+import Meetings from '/imports/api/meetings';
+import Users from '/imports/api/users';
+import { publicationSafeGuard } from '/imports/api/common/server/helpers';
+
+const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
+
+function breakoutsHistory() {
+ const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id });
+
+ if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
+ Logger.warn(`Publishing Meetings-history was requested by unauth connection ${this.connection.id}`);
+ return Meetings.find({ meetingId: '' });
+ }
+
+ const { meetingId, userId } = tokenValidation;
+ Logger.debug('Publishing Breakouts-History', { meetingId, userId });
+
+ const User = Users.findOne({ userId, meetingId }, { fields: { userId: 1, role: 1 } });
+ if (!User || User.role !== ROLE_MODERATOR) {
+ return BreakoutsHistory.find({ meetingId: '' });
+ }
+
+ check(meetingId, String);
+
+ const selector = {
+ meetingId,
+ };
+
+ // Monitor this publication and stop it when user is not a moderator anymore
+ const comparisonFunc = () => {
+ const user = Users.findOne({ userId, meetingId }, { fields: { role: 1, userId: 1 } });
+ const condition = user.role === ROLE_MODERATOR;
+
+ if (!condition) {
+ Logger.info(`conditions aren't filled anymore in publication ${this._name}:
+ user.role === ROLE_MODERATOR :${condition}, user.role: ${user.role} ROLE_MODERATOR: ${ROLE_MODERATOR}`);
+ }
+
+ return condition;
+ };
+ publicationSafeGuard(comparisonFunc, this);
+
+ return BreakoutsHistory.find(selector);
+}
+
+function publish(...args) {
+ const boundUsers = breakoutsHistory.bind(this);
+ return boundUsers(...args);
+}
+
+Meteor.publish('breakouts-history', publish);
diff --git a/bigbluebutton-html5/imports/api/breakouts/server/eventHandlers.js b/bigbluebutton-html5/imports/api/breakouts/server/eventHandlers.js
index 8628c3e2dd..d179e4164b 100755
--- a/bigbluebutton-html5/imports/api/breakouts/server/eventHandlers.js
+++ b/bigbluebutton-html5/imports/api/breakouts/server/eventHandlers.js
@@ -1,13 +1,12 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleBreakoutJoinURL from './handlers/breakoutJoinURL';
-import handleBreakoutStarted from './handlers/breakoutStarted';
+import handleBreakoutRoomsList from './handlers/breakoutList';
import handleUpdateTimeRemaining from './handlers/updateTimeRemaining';
import handleBreakoutClosed from './handlers/breakoutClosed';
import joinedUsersChanged from './handlers/joinedUsersChanged';
-RedisPubSub.on('BreakoutRoomStartedEvtMsg', handleBreakoutStarted);
+RedisPubSub.on('BreakoutRoomsListEvtMsg', handleBreakoutRoomsList);
RedisPubSub.on('BreakoutRoomJoinURLEvtMsg', handleBreakoutJoinURL);
-RedisPubSub.on('RequestBreakoutJoinURLRespMsg', handleBreakoutJoinURL);
RedisPubSub.on('BreakoutRoomsTimeRemainingUpdateEvtMsg', handleUpdateTimeRemaining);
RedisPubSub.on('BreakoutRoomEndedEvtMsg', handleBreakoutClosed);
RedisPubSub.on('UpdateBreakoutUsersEvtMsg', joinedUsersChanged);
diff --git a/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutList.js b/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutList.js
new file mode 100644
index 0000000000..7de9c5d5d6
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutList.js
@@ -0,0 +1,60 @@
+import Breakouts from '/imports/api/breakouts';
+import Logger from '/imports/startup/server/logger';
+import { check } from 'meteor/check';
+import flat from 'flat';
+import handleBreakoutRoomsListHist from '/imports/api/breakouts-history/server/handlers/breakoutRoomsList';
+
+export default function handleBreakoutRoomsList({ body }, meetingId) {
+ // 0 seconds default breakout time, forces use of real expiration time
+ const DEFAULT_TIME_REMAINING = 0;
+
+ const {
+ meetingId: parentMeetingId,
+ rooms,
+ } = body;
+
+ // set firstly the last seq, then client will know when receive all
+ rooms.sort((a, b) => ((a.sequence < b.sequence) ? 1 : -1)).forEach((breakout) => {
+ const { breakoutId, html5JoinUrls, ...breakoutWithoutUrls } = breakout;
+
+ check(meetingId, String);
+
+ const selector = {
+ breakoutId,
+ };
+
+ const urls = {};
+ if (typeof html5JoinUrls === 'object' && Object.keys(html5JoinUrls).length > 0) {
+ Object.keys(html5JoinUrls).forEach((userId) => {
+ urls[`url_${userId}`] = {
+ redirectToHtml5JoinURL: html5JoinUrls[userId],
+ insertedTime: new Date().getTime(),
+ };
+ });
+ }
+
+ const modifier = {
+ $set: {
+ breakoutId,
+ joinedUsers: [],
+ timeRemaining: DEFAULT_TIME_REMAINING,
+ parentMeetingId,
+ ...flat(breakoutWithoutUrls),
+ ...urls,
+ },
+ };
+
+ try {
+ const { numberAffected } = Breakouts.upsert(selector, modifier);
+
+ if (numberAffected) {
+ Logger.info('Updated timeRemaining and externalMeetingId '
+ + `for breakout id=${breakoutId}`);
+ }
+ } catch (err) {
+ Logger.error(`updating breakout: ${err}`);
+ }
+ });
+
+ handleBreakoutRoomsListHist({ body });
+}
diff --git a/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutStarted.js b/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutStarted.js
deleted file mode 100644
index 47a0afe1fc..0000000000
--- a/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutStarted.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import Breakouts from '/imports/api/breakouts';
-import Logger from '/imports/startup/server/logger';
-import { check } from 'meteor/check';
-import flat from 'flat';
-
-export default function handleBreakoutRoomStarted({ body }, meetingId) {
- // 0 seconds default breakout time, forces use of real expiration time
- const DEFAULT_TIME_REMAINING = 0;
-
- const {
- parentMeetingId,
- breakout,
- } = body;
-
- const { breakoutId } = breakout;
-
- check(meetingId, String);
-
- const selector = {
- breakoutId,
- };
-
- const modifier = {
- $set: Object.assign(
- {
- joinedUsers: [],
- },
- { timeRemaining: DEFAULT_TIME_REMAINING },
- { parentMeetingId },
- flat(breakout),
- ),
- };
-
- try {
- const { numberAffected } = Breakouts.upsert(selector, modifier);
-
- if (numberAffected) {
- Logger.info('Updated timeRemaining and externalMeetingId '
- + `for breakout id=${breakoutId}`);
- }
- } catch (err) {
- Logger.error(`updating breakout: ${err}`);
- }
-}
diff --git a/bigbluebutton-html5/imports/api/breakouts/server/handlers/joinedUsersChanged.js b/bigbluebutton-html5/imports/api/breakouts/server/handlers/joinedUsersChanged.js
index 4918a3b8ac..44cdefc0a6 100644
--- a/bigbluebutton-html5/imports/api/breakouts/server/handlers/joinedUsersChanged.js
+++ b/bigbluebutton-html5/imports/api/breakouts/server/handlers/joinedUsersChanged.js
@@ -1,4 +1,5 @@
import Breakouts from '/imports/api/breakouts';
+import updateUserBreakoutRoom from '/imports/api/users-persistent-data/server/modifiers/updateUserBreakoutRoom';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
@@ -31,6 +32,8 @@ export default function joinedUsersChanged({ body }) {
const numberAffected = Breakouts.update(selector, modifier);
if (numberAffected) {
+ updateUserBreakoutRoom(parentId, breakoutId, users);
+
Logger.info(`Updated joined users in breakout id=${breakoutId}`);
}
} catch (err) {
diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
index a7bf664b7a..ffc681cee9 100755
--- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
+++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
@@ -117,6 +117,7 @@ export default function addMeeting(meeting) {
systemProps: {
html5InstanceId: Number,
},
+ groups: Array,
});
const {
diff --git a/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/updateUserBreakoutRoom.js b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/updateUserBreakoutRoom.js
new file mode 100644
index 0000000000..9fe78b8710
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/updateUserBreakoutRoom.js
@@ -0,0 +1,39 @@
+import { check } from 'meteor/check';
+import UsersPersistentData from '/imports/api/users-persistent-data';
+import Logger from '/imports/startup/server/logger';
+import Breakouts from '/imports/api/breakouts';
+
+export default function updateUserBreakoutRoom(meetingId, breakoutId, users) {
+ check(meetingId, String);
+ check(breakoutId, String);
+ check(users, Array);
+
+ const lastBreakoutRoom = Breakouts.findOne({ breakoutId }, {
+ fields: {
+ isDefaultName: 1,
+ sequence: 1,
+ shortName: 1,
+ },
+ });
+
+ users.forEach((user) => {
+ const userId = user.id.substr(0, user.id.lastIndexOf('-'));
+
+ const selector = {
+ userId,
+ meetingId,
+ };
+
+ const modifier = {
+ $set: {
+ lastBreakoutRoom,
+ },
+ };
+
+ try {
+ UsersPersistentData.update(selector, modifier);
+ } catch (err) {
+ Logger.error(`Updating users persistent data's lastBreakoutRoom to the collection: ${err}`);
+ }
+ });
+}
diff --git a/bigbluebutton-html5/imports/api/users-persistent-data/server/publishers.js b/bigbluebutton-html5/imports/api/users-persistent-data/server/publishers.js
index b634afd05c..5160bef510 100644
--- a/bigbluebutton-html5/imports/api/users-persistent-data/server/publishers.js
+++ b/bigbluebutton-html5/imports/api/users-persistent-data/server/publishers.js
@@ -2,6 +2,9 @@ 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';
+import Users from '/imports/api/users';
+
+const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
function usersPersistentData() {
if (!this.userId) {
@@ -16,7 +19,16 @@ function usersPersistentData() {
meetingId,
};
- return UsersPersistentData.find(selector);
+ const options = {};
+
+ const User = Users.findOne({ userId: requesterUserId, meetingId }, { fields: { role: 1 } });
+ if (!User || User.role !== ROLE_MODERATOR) {
+ options.fields = {
+ lastBreakoutRoom: false,
+ };
+ }
+
+ return UsersPersistentData.find(selector, options);
}
function publishUsersPersistentData(...args) {
diff --git a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js
index 2f6a93d464..b40818349c 100644
--- a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js
+++ b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js
@@ -19,8 +19,6 @@ const oldParameters = {
listenOnlyMode: 'bbb_listen_only_mode',
multiUserPenOnly: 'bbb_multi_user_pen_only',
multiUserTools: 'bbb_multi_user_tools',
- outsideToggleRecording: 'bbb_outside_toggle_recording',
- outsideToggleSelfVoice: 'bbb_outside_toggle_self_voice',
presenterTools: 'bbb_presenter_tools',
shortcuts: 'bbb_shortcuts',
skipCheck: 'bbb_skip_check_audio',
@@ -67,9 +65,6 @@ const currentParameters = [
'bbb_show_public_chat_on_login',
'bbb_hide_actions_bar',
'bbb_hide_nav_bar',
- // OUTSIDE COMMANDS
- 'bbb_outside_toggle_self_voice',
- 'bbb_outside_toggle_recording',
];
function valueParser(val) {
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx
index 42ab536403..f3582328e0 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx
@@ -37,6 +37,14 @@ const intlMessages = defineMessages({
id: 'app.createBreakoutRoom.durationInMinutes',
description: 'duration time label',
},
+ resetAssignments: {
+ id: 'app.createBreakoutRoom.resetAssignments',
+ description: 'reset assignments label',
+ },
+ resetAssignmentsDesc: {
+ id: 'app.createBreakoutRoom.resetAssignmentsDesc',
+ description: 'reset assignments label description',
+ },
randomlyAssign: {
id: 'app.createBreakoutRoom.randomlyAssign',
description: 'randomly assign label',
@@ -165,6 +173,7 @@ class BreakoutRoom extends PureComponent {
this.setFreeJoin = this.setFreeJoin.bind(this);
this.getUserByRoom = this.getUserByRoom.bind(this);
this.onAssignRandomly = this.onAssignRandomly.bind(this);
+ this.onAssignReset = this.onAssignReset.bind(this);
this.onInviteBreakout = this.onInviteBreakout.bind(this);
this.renderUserItemByRoom = this.renderUserItemByRoom.bind(this);
this.renderRoomsGrid = this.renderRoomsGrid.bind(this);
@@ -210,16 +219,24 @@ class BreakoutRoom extends PureComponent {
}
componentDidMount() {
- const { isInvitation, breakoutJoinedUsers } = this.props;
+ const {
+ isInvitation, breakoutJoinedUsers, getLastBreakouts, groups,
+ } = this.props;
this.setRoomUsers();
if (isInvitation) {
this.setInvitationConfig();
- }
- if (isInvitation) {
+
this.setState({
breakoutJoinedUsers,
});
}
+
+ const lastBreakouts = getLastBreakouts();
+ if (lastBreakouts.length > 0) {
+ this.populateWithLastBreakouts(lastBreakouts);
+ } else if (groups && groups.length > 0) {
+ this.populateWithGroups(groups);
+ }
}
componentDidUpdate(prevProps, prevstate) {
@@ -427,6 +444,16 @@ class BreakoutRoom extends PureComponent {
}
}
+ onAssignReset() {
+ const { users } = this.state;
+
+ users.forEach((u) => {
+ if (u.room !== null && u.room > 0) {
+ this.changeUserRoom(u.userId, 0);
+ }
+ });
+ }
+
setInvitationConfig() {
const { getBreakouts } = this.props;
this.setState({
@@ -583,6 +610,68 @@ class BreakoutRoom extends PureComponent {
return false;
}
+ populateWithLastBreakouts(lastBreakouts) {
+ const { getBreakoutUserWasIn, users, intl } = this.props;
+
+ const changedNames = [];
+ lastBreakouts.forEach((breakout) => {
+ if (breakout.isDefaultName === false) {
+ changedNames[breakout.sequence] = breakout.shortName;
+ }
+ });
+
+ this.setState({
+ roomNamesChanged: changedNames,
+ numberOfRooms: lastBreakouts.length,
+ roomNameDuplicatedIsValid: true,
+ roomNameEmptyIsValid: true,
+ }, () => {
+ const rooms = _.range(1, lastBreakouts.length + 1).map((seq) => this.getRoomName(seq));
+
+ users.forEach((u) => {
+ const lastUserBreakout = getBreakoutUserWasIn(u.userId, u.extId);
+ if (lastUserBreakout !== null) {
+ const lastUserBreakoutName = lastUserBreakout.isDefaultName === false
+ ? lastUserBreakout.shortName
+ : intl.formatMessage(intlMessages.breakoutRoom, { 0: lastUserBreakout.sequence });
+
+ if (rooms.indexOf(lastUserBreakoutName) !== false) {
+ this.changeUserRoom(u.userId, rooms.indexOf(lastUserBreakoutName) + 1);
+ }
+ }
+ });
+ });
+ }
+
+ populateWithGroups(groups) {
+ const { users } = this.props;
+
+ const changedNames = [];
+ groups.forEach((group, idx) => {
+ if (group.name.length > 0) {
+ changedNames[idx + 1] = group.name;
+ }
+ });
+
+ this.setState({
+ roomNamesChanged: changedNames,
+ numberOfRooms: groups.length > 1 ? groups.length : 2,
+ roomNameDuplicatedIsValid: true,
+ roomNameEmptyIsValid: true,
+ }, () => {
+ groups.forEach((group, groupIdx) => {
+ const usersInGroup = group.usersExtId;
+ if (usersInGroup.length > 0) {
+ usersInGroup.forEach((groupUserExtId) => {
+ users.filter((u) => u.extId === groupUserExtId).forEach((foundUser) => {
+ this.changeUserRoom(foundUser.userId, groupIdx + 1);
+ });
+ });
+ }
+ });
+ });
+ }
+
renderRoomsGrid() {
const { intl, isInvitation } = this.props;
const {
@@ -764,15 +853,26 @@ class BreakoutRoom extends PureComponent {
}
-
+
+
+
+
{intl.formatMessage(intlMessages.numberOfRoomsIsValid)}
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/container.jsx
index d6e5b4445b..b5a1530f7e 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/container.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import ActionsBarService from '/imports/ui/components/actions-bar/service';
+import BreakoutRoomService from '/imports/ui/components/breakout-room/service';
import CreateBreakoutRoomModal from './component';
@@ -17,10 +18,13 @@ const CreateBreakoutRoomContainer = (props) => {
export default withTracker(() => ({
createBreakoutRoom: ActionsBarService.createBreakoutRoom,
getBreakouts: ActionsBarService.getBreakouts,
+ getLastBreakouts: ActionsBarService.getLastBreakouts,
+ getBreakoutUserWasIn: BreakoutRoomService.getBreakoutUserWasIn,
getUsersNotAssigned: ActionsBarService.getUsersNotAssigned,
sendInvitation: ActionsBarService.sendInvitation,
breakoutJoinedUsers: ActionsBarService.breakoutJoinedUsers(),
users: ActionsBarService.users(),
+ groups: ActionsBarService.groups(),
isMe: ActionsBarService.isMe,
meetingName: ActionsBarService.meetingName(),
amIModerator: ActionsBarService.amIModerator(),
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/styles.js b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/styles.js
index b7317f63a9..82efb39464 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/styles.js
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/styles.js
@@ -190,7 +190,11 @@ const HoldButtonWrapper = styled(HoldButton)`
}
`;
-const RandomlyAssignBtn = styled(Button)`
+const AssignBtnsContainer = styled.div`
+ margin-top: auto;
+`;
+
+const AssignBtns = styled(Button)`
color: ${colorPrimary};
font-size: ${fontSizeSmall};
white-space: nowrap;
@@ -302,7 +306,8 @@ export default {
DurationArea,
DurationInput,
HoldButtonWrapper,
- RandomlyAssignBtn,
+ AssignBtnsContainer,
+ AssignBtns,
CheckBoxesContainer,
FreeJoinCheckbox,
RoomUserItem,
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
index 2f295970af..146ab005c1 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
@@ -4,6 +4,7 @@ import { makeCall } from '/imports/ui/services/api';
import Meetings from '/imports/api/meetings';
import Breakouts from '/imports/api/breakouts';
import { getVideoUrl } from '/imports/ui/components/external-video-player/service';
+import BreakoutsHistory from '/imports/api/breakouts-history';
const USER_CONFIG = Meteor.settings.public.user;
const ROLE_MODERATOR = USER_CONFIG.role_moderator;
@@ -13,6 +14,16 @@ const getBreakouts = () => Breakouts.find({ parentMeetingId: Auth.meetingID })
.fetch()
.sort((a, b) => a.sequence - b.sequence);
+const getLastBreakouts = () => {
+ const lastBreakouts = BreakoutsHistory.findOne({ meetingId: Auth.meetingID });
+ if (lastBreakouts) {
+ return lastBreakouts.rooms
+ .sort((a, b) => a.sequence - b.sequence);
+ }
+
+ return [];
+};
+
const currentBreakoutUsers = (user) => !Breakouts.findOne({
'joinedUsers.userId': new RegExp(`^${user.userId}`),
});
@@ -47,6 +58,8 @@ export default {
meetingId: Auth.meetingID,
clientType: { $ne: DIAL_IN_USER },
}).fetch(),
+ groups: () => Meetings.findOne({ meetingId: Auth.meetingID },
+ { fields: { groups: 1 } }).groups,
isBreakoutEnabled: () => Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { 'breakoutProps.enabled': 1 } }).breakoutProps.enabled,
isBreakoutRecordable: () => Meetings.findOne({ meetingId: Auth.meetingID },
@@ -58,6 +71,7 @@ export default {
joinedUsers: { $exists: true },
}, { fields: { joinedUsers: 1, breakoutId: 1, sequence: 1 }, sort: { sequence: 1 } }).fetch(),
getBreakouts,
+ getLastBreakouts,
getUsersNotAssigned,
takePresenterRole,
isSharingVideo: () => getVideoUrl(),
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
index d3fd47cead..f1d10516b6 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
@@ -30,7 +30,6 @@ const intlMessages = defineMessages({
const propTypes = {
shortcuts: PropTypes.objectOf(PropTypes.string).isRequired,
- processToggleMuteFromOutside: PropTypes.func.isRequired,
handleToggleMuteMicrophone: PropTypes.func.isRequired,
handleJoinAudio: PropTypes.func.isRequired,
handleLeaveAudio: PropTypes.func.isRequired,
@@ -54,14 +53,6 @@ class AudioControls extends PureComponent {
this.renderJoinLeaveButton = this.renderJoinLeaveButton.bind(this);
}
- componentDidMount() {
- const { processToggleMuteFromOutside } = this.props;
- if (Meteor.settings.public.allowOutsideCommands.toggleSelfVoice
- || getFromUserSettings('bbb_outside_toggle_self_voice', false)) {
- window.addEventListener('message', processToggleMuteFromOutside);
- }
- }
-
renderJoinButton() {
const {
handleJoinAudio,
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
index 2e6bda4c36..bedaabee5f 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
@@ -2,7 +2,6 @@ import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/modal/service';
import AudioManager from '/imports/ui/services/audio-manager';
-import { makeCall } from '/imports/ui/services/api';
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
import { withUsersConsumer } from '/imports/ui/components/components-data/users-context/context';
import logger from '/imports/startup/client/logger';
@@ -29,28 +28,6 @@ const AudioControlsContainer = (props) => {
return ;
};
-const processToggleMuteFromOutside = (e) => {
- switch (e.data) {
- case 'c_mute': {
- makeCall('toggleVoice');
- break;
- }
- case 'get_audio_joined_status': {
- const audioJoinedState = AudioManager.isConnected ? 'joinedAudio' : 'notInAudio';
- this.window.parent.postMessage({ response: audioJoinedState }, '*');
- break;
- }
- case 'c_mute_status': {
- const muteState = AudioManager.isMuted ? 'selfMuted' : 'selfUnmuted';
- this.window.parent.postMessage({ response: muteState }, '*');
- break;
- }
- default: {
- // console.log(e.data);
- }
- }
-};
-
const handleLeaveAudio = () => {
const meetingIsBreakout = AppService.meetingIsBreakout();
@@ -100,7 +77,6 @@ export default withUsersConsumer(
}
return ({
- processToggleMuteFromOutside: (arg) => processToggleMuteFromOutside(arg),
showMute: isConnected() && !isListenOnly() && !isEchoTest() && !userLocks.userMic,
muted: isConnected() && !isListenOnly() && isMuted(),
inAudio: isConnected() && !isEchoTest(),
diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/component.jsx
index 0bea15f742..1bd4763849 100644
--- a/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/component.jsx
@@ -66,25 +66,27 @@ class BreakoutRoomInvitation extends Component {
const hasBreakouts = breakouts.length > 0;
if (hasBreakouts && !breakoutUserIsIn && BreakoutService.checkInviteModerators()) {
- // Have to check for freeJoin breakouts first because currentBreakoutUrlData will
- // populate after a room has been joined
- const breakoutRoom = getBreakoutByUrlData(currentBreakoutUrlData);
- const freeJoinBreakout = breakouts.find((breakout) => breakout.freeJoin);
- if (freeJoinBreakout) {
- if (!didSendBreakoutInvite) {
- this.inviteUserToBreakout(breakoutRoom || freeJoinBreakout);
- this.setState({ didSendBreakoutInvite: true });
- }
- } else if (currentBreakoutUrlData) {
+ const freeJoinRooms = breakouts.filter((breakout) => breakout.freeJoin);
+
+ if (currentBreakoutUrlData) {
+ const breakoutRoom = getBreakoutByUrlData(currentBreakoutUrlData);
const currentInsertedTime = currentBreakoutUrlData.insertedTime;
const oldCurrentUrlData = oldProps.currentBreakoutUrlData || {};
const oldInsertedTime = oldCurrentUrlData.insertedTime;
if (currentInsertedTime !== oldInsertedTime) {
- const breakoutId = Session.get('lastBreakoutOpened');
- if (breakoutRoom.breakoutId !== breakoutId) {
+ const lastBreakoutId = Session.get('lastBreakoutOpened');
+ if (breakoutRoom.breakoutId !== lastBreakoutId) {
this.inviteUserToBreakout(breakoutRoom);
}
}
+ } else if (freeJoinRooms.length > 0 && !didSendBreakoutInvite) {
+ const maxSeq = Math.max(...freeJoinRooms.map(((room) => room.sequence)));
+ // Check if received all rooms and Pick a room randomly
+ if (maxSeq === freeJoinRooms.length) {
+ const randomRoom = freeJoinRooms[Math.floor(Math.random() * freeJoinRooms.length)];
+ this.inviteUserToBreakout(randomRoom);
+ this.setState({ didSendBreakoutInvite: true });
+ }
}
}
diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/service.js b/bigbluebutton-html5/imports/ui/components/breakout-room/service.js
index 1313b4293e..56add6a0a4 100644
--- a/bigbluebutton-html5/imports/ui/components/breakout-room/service.js
+++ b/bigbluebutton-html5/imports/ui/components/breakout-room/service.js
@@ -1,11 +1,11 @@
import Breakouts from '/imports/api/breakouts';
-import { MeetingTimeRemaining } from '/imports/api/meetings';
-import Meetings from '/imports/api/meetings';
+import { MeetingTimeRemaining, Meetings } from '/imports/api/meetings';
import { makeCall } from '/imports/ui/services/api';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import UserListService from '/imports/ui/components/user-list/service';
import fp from 'lodash/fp';
+import UsersPersistentData from '/imports/api/users-persistent-data';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
@@ -155,6 +155,33 @@ const getBreakoutUserIsIn = (userId) =>
{ fields: { sequence: 1 } }
);
+const getBreakoutUserWasIn = (userId, extId) => {
+ const selector = {
+ meetingId: Auth.meetingID,
+ lastBreakoutRoom: { $exists: 1 },
+ };
+
+ if (extId !== null) {
+ selector.extId = extId;
+ } else {
+ selector.userId = userId;
+ }
+
+ const users = UsersPersistentData.find(
+ selector,
+ { fields: { userId: 1, lastBreakoutRoom: 1 } },
+ ).fetch();
+
+ if (users.length > 0) {
+ const hasCurrUserId = users.filter((user) => user.userId === userId);
+ if (hasCurrUserId.length > 0) return hasCurrUserId.pop().lastBreakoutRoom;
+
+ return users.pop().lastBreakoutRoom;
+ }
+
+ return null;
+};
+
const isUserInBreakoutRoom = (joinedUsers) => {
const userId = Auth.userID;
@@ -177,6 +204,7 @@ export default {
getBreakouts,
getBreakoutsNoTime,
getBreakoutUserIsIn,
+ getBreakoutUserWasIn,
sortUsersByName: UserListService.sortUsersByName,
isUserInBreakoutRoom,
checkInviteModerators,
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx
index 8bdf6313ad..99ef2eb7ef 100755
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx
@@ -2,7 +2,6 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withModalMounter } from '/imports/ui/components/modal/service';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
-import getFromUserSettings from '/imports/ui/services/users-settings';
import { defineMessages, injectIntl } from 'react-intl';
import Styled from './styles';
import RecordingIndicator from './recording-indicator/container';
@@ -50,20 +49,12 @@ class NavBar extends Component {
componentDidMount() {
const {
- processOutsideToggleRecording,
- connectRecordingObserver,
shortcuts: TOGGLE_USERLIST_AK,
} = this.props;
const { isFirefox } = browserInfo;
const { isMacos } = deviceInfo;
- if (Meteor.settings.public.allowOutsideCommands.toggleRecording
- || getFromUserSettings('bbb_outside_toggle_recording', false)) {
- connectRecordingObserver();
- window.addEventListener('message', processOutsideToggleRecording);
- }
-
// accessKey U does not work on firefox for macOS for some unknown reason
if (isMacos && isFirefox && TOGGLE_USERLIST_AK === 'U') {
document.addEventListener('keyup', (event) => {
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx
index ef299cdb53..b3c12c9319 100755
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx
@@ -9,7 +9,6 @@ import { ChatContext } from '/imports/ui/components/components-data/chat-context
import { GroupChatContext } from '/imports/ui/components/components-data/group-chat-context/context';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import NoteService from '/imports/ui/components/note/service';
-import Service from './service';
import NavBar from './component';
import { layoutSelectInput, layoutSelectOutput, layoutDispatch } from '../layout/context';
@@ -100,12 +99,8 @@ export default withTracker(() => {
document.title = titleString;
}
- const { connectRecordingObserver, processOutsideToggleRecording } = Service;
-
return {
currentUserId: Auth.userID,
- processOutsideToggleRecording,
- connectRecordingObserver,
meetingId,
presentationTitle: meetingTitle,
};
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/container.jsx
index b29c5364e4..c2a31bd3b3 100644
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/container.jsx
@@ -15,18 +15,6 @@ export default withTracker(() => {
const meetingId = Auth.meetingID;
const recordObeject = RecordMeetings.findOne({ meetingId });
- RecordMeetings.find({ meetingId: Auth.meetingID }, { fields: { recording: 1 } }).observeChanges({
- changed: (id, fields) => {
- if (fields && fields.recording) {
- this.window.parent.postMessage({ response: 'recordingStarted' }, '*');
- }
-
- if (fields && !fields.recording) {
- this.window.parent.postMessage({ response: 'recordingStopped' }, '*');
- }
- },
- });
-
const micUser = VoiceUsers.findOne({ meetingId, joined: true, listenOnly: false }, {
fields: {
joined: 1,
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/service.js b/bigbluebutton-html5/imports/ui/components/nav-bar/service.js
deleted file mode 100755
index 6857f00219..0000000000
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/service.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import Auth from '/imports/ui/services/auth';
-import { makeCall } from '/imports/ui/services/api';
-import Meetings from '/imports/api/meetings';
-
-const processOutsideToggleRecording = (e) => {
- switch (e.data) {
- case 'c_record': {
- makeCall('toggleRecording');
- break;
- }
- case 'c_recording_status': {
- const recordingState = (Meetings.findOne({ meetingId: Auth.meetingID })).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' }, '*');
-};
-
-export default {
- connectRecordingObserver: () => connectRecordingObserver(),
- processOutsideToggleRecording: arg => processOutsideToggleRecording(arg),
-};
diff --git a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx
index 1f72ec79a9..c8065ab46e 100755
--- a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx
@@ -9,6 +9,7 @@ import AnnotationsTextService from '/imports/ui/components/whiteboard/annotation
import { Annotations as AnnotationsLocal } from '/imports/ui/components/whiteboard/service';
import {
localBreakoutsSync,
+ localBreakoutsHistorySync,
localGuestUsersSync,
localMeetingsSync,
localUsersSync,
@@ -25,7 +26,7 @@ const SUBSCRIPTIONS = [
'voiceUsers', 'whiteboard-multi-user', 'screenshare', 'group-chat',
'presentation-pods', 'users-settings', 'guestUser', 'users-infos', 'note', 'meeting-time-remaining',
'local-settings', 'users-typing', 'record-meetings', 'video-streams',
- 'connection-status', 'voice-call-states', 'external-video-meetings', 'breakouts',
+ 'connection-status', 'voice-call-states', 'external-video-meetings', 'breakouts', 'breakouts-history',
];
const EVENT_NAME = 'bbb-group-chat-messages-subscription-has-stoppped';
@@ -101,6 +102,7 @@ export default withTracker(() => {
// let this withTracker re-execute when a subscription is stopped
subscriptionReactivity.depend();
localBreakoutsSync.setIgnoreDeletes(true);
+ localBreakoutsHistorySync.setIgnoreDeletes(true);
localGuestUsersSync.setIgnoreDeletes(true);
localMeetingsSync.setIgnoreDeletes(true);
localUsersSync.setIgnoreDeletes(true);
@@ -110,6 +112,7 @@ export default withTracker(() => {
SubscriptionRegistry.getSubscription('meetings'),
SubscriptionRegistry.getSubscription('users'),
SubscriptionRegistry.getSubscription('breakouts'),
+ SubscriptionRegistry.getSubscription('breakouts-history'),
SubscriptionRegistry.getSubscription('guestUser'),
].forEach((item) => {
if (item) item.stop();
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
index f9b84e19f6..b38c0e8369 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
@@ -40,6 +40,7 @@ class UserListItem extends PureComponent {
toggleUserLock,
requestUserInformation,
userInBreakout,
+ userLastBreakout,
breakoutSequence,
meetingIsBreakout,
isMeteorConnected,
@@ -78,6 +79,7 @@ class UserListItem extends PureComponent {
toggleUserLock,
requestUserInformation,
userInBreakout,
+ userLastBreakout,
breakoutSequence,
meetingIsBreakout,
isMeteorConnected,
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx
index 77bbc191f2..2d7b9035d1 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx
@@ -16,6 +16,7 @@ const isMe = (intId) => intId === Auth.userID;
export default withTracker(({ user }) => {
const findUserInBreakout = BreakoutService.getBreakoutUserIsIn(user.userId);
+ const findUserLastBreakout = BreakoutService.getBreakoutUserWasIn(user.userId, null);
const breakoutSequence = (findUserInBreakout || {}).sequence;
const Meeting = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { lockSettingsProps: 1 } });
@@ -24,6 +25,7 @@ export default withTracker(({ user }) => {
user,
isMe,
userInBreakout: !!findUserInBreakout,
+ userLastBreakout: findUserLastBreakout,
breakoutSequence,
lockSettingsProps: Meeting && Meeting.lockSettingsProps,
isMeteorConnected: Meteor.status().connected,
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
index 153e23f155..f424aa2c73 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
@@ -572,6 +572,7 @@ class UserDropdown extends PureComponent {
user,
intl,
isThisMeetingLocked,
+ userLastBreakout,
isMe,
isRTL,
} = this.props;
@@ -612,6 +613,7 @@ class UserDropdown extends PureComponent {
isThisMeetingLocked,
userAriaLabel,
isActionsOpen,
+ userLastBreakout,
isMe,
}}
/>
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-name/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-name/component.jsx
index 0115e41557..75715a3122 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-name/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-name/component.jsx
@@ -42,6 +42,10 @@ const messages = defineMessages({
id: 'app.userList.userAriaLabel',
description: 'aria label for each user in the userlist',
},
+ breakoutRoom: {
+ id: 'app.createBreakoutRoom.room',
+ description: 'breakout room',
+ },
});
const propTypes = {
@@ -68,6 +72,7 @@ const UserName = (props) => {
isThisMeetingLocked,
userAriaLabel,
isActionsOpen,
+ userLastBreakout,
isMe,
user,
} = props;
@@ -110,6 +115,18 @@ const UserName = (props) => {
if (LABEL.guest) userNameSub.push(intl.formatMessage(messages.guest));
}
+ if (userLastBreakout) {
+ userNameSub.push(
+
+
+
+ {userLastBreakout.isDefaultName
+ ? intl.formatMessage(messages.breakoutRoom, { 0: userLastBreakout.sequence })
+ : userLastBreakout.shortName}
+ ,
+ );
+ }
+
return (
{
} = params;
const fetchFunc = published
- ? AnnotationGroupService.getCurrentAnnotationsInfo : AnnotationGroupService.getUnsetAnnotations;
+ ? AnnotationGroupService.getCurrentAnnotationsInfo : AnnotationGroupService.getUnsentAnnotations;
const annotationsInfo = fetchFunc(whiteboardId);
return {
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/annotation-group/service.js b/bigbluebutton-html5/imports/ui/components/whiteboard/annotation-group/service.js
index 683b8449a7..84097afb50 100755
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/annotation-group/service.js
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/annotation-group/service.js
@@ -16,7 +16,7 @@ const getCurrentAnnotationsInfo = (whiteboardId) => {
).fetch();
};
-const getUnsetAnnotations = (whiteboardId) => {
+const getUnsentAnnotations = (whiteboardId) => {
if (!whiteboardId) {
return null;
}
@@ -34,5 +34,5 @@ const getUnsetAnnotations = (whiteboardId) => {
export default {
getCurrentAnnotationsInfo,
- getUnsetAnnotations,
+ getUnsentAnnotations,
};
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
index f0a3253873..a7b7b1b066 100755
--- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -393,8 +393,6 @@ class AudioManager {
muteState = 'selfUnmuted';
this.unmute();
}
-
- window.parent.postMessage({ response: muteState }, '*');
}
if (fields.talking !== undefined && fields.talking !== this.isTalking) {
@@ -431,7 +429,6 @@ class AudioManager {
}
if (!this.isEchoTest) {
- window.parent.postMessage({ response: 'joinedAudio' }, '*');
this.notify(this.intl.formatMessage(this.messages.info.JOINED_AUDIO));
logger.info({ logCode: 'audio_joined' }, 'Audio Joined');
this.inputStream = (this.bridge ? this.bridge.inputStream : null);
@@ -473,7 +470,6 @@ class AudioManager {
this.playHangUpSound();
}
- window.parent.postMessage({ response: 'notInAudio' }, '*');
window.removeEventListener('audioPlayFailed', this.handlePlayElementFailed);
}
diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json
index cbe1916288..7dd9d75e56 100644
--- a/bigbluebutton-html5/package-lock.json
+++ b/bigbluebutton-html5/package-lock.json
@@ -3574,6 +3574,64 @@
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
+ "meow": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
+ "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==",
+ "requires": {
+ "@types/minimist": "^1.2.0",
+ "camelcase-keys": "^6.2.2",
+ "decamelize": "^1.2.0",
+ "decamelize-keys": "^1.1.0",
+ "hard-rejection": "^2.1.0",
+ "minimist-options": "4.1.0",
+ "normalize-package-data": "^3.0.0",
+ "read-pkg-up": "^7.0.1",
+ "redent": "^3.0.0",
+ "trim-newlines": "^3.0.0",
+ "type-fest": "^0.18.0",
+ "yargs-parser": "^20.2.3"
+ },
+ "dependencies": {
+ "hosted-git-info": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz",
+ "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==",
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "normalize-package-data": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.2.tgz",
+ "integrity": "sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg==",
+ "requires": {
+ "hosted-git-info": "^4.0.1",
+ "resolve": "^1.20.0",
+ "semver": "^7.3.4",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "trim-newlines": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.0.2.tgz",
+ "integrity": "sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew=="
+ },
+ "type-fest": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz",
+ "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw=="
+ }
+ }
+ },
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index 6e6d3b3061..5541ee8b69 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -411,9 +411,6 @@ public:
syncUsersWithConnectionManager:
enabled: false
syncInterval: 60000
- allowOutsideCommands:
- toggleRecording: false
- toggleSelfVoice: false
poll:
enabled: true
maxCustom: 5
diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json
index fc8c73c147..98001c031e 100755
--- a/bigbluebutton-html5/public/locales/en.json
+++ b/bigbluebutton-html5/public/locales/en.json
@@ -847,6 +847,8 @@
"app.createBreakoutRoom.durationInMinutes": "Duration (minutes)",
"app.createBreakoutRoom.randomlyAssign": "Randomly assign",
"app.createBreakoutRoom.randomlyAssignDesc": "Assigns users randomly to breakout rooms",
+ "app.createBreakoutRoom.resetAssignments": "Reset assignments",
+ "app.createBreakoutRoom.resetAssignmentsDesc": "Reset all user room assignments",
"app.createBreakoutRoom.endAllBreakouts": "End all breakout rooms",
"app.createBreakoutRoom.roomName": "{0} (Room - {1})",
"app.createBreakoutRoom.doneLabel": "Done",
@@ -926,15 +928,18 @@
"playback.player.thumbnails.wrapper.aria": "Thumbnails area",
"playback.player.video.wrapper.aria": "Video area",
"app.learningDashboard.dashboardTitle": "Learning Dashboard",
- "app.learningDashboard.user": "User",
+ "app.learningDashboard.downloadSessionDataLabel": "Download Session Data",
+ "app.learningDashboard.lastUpdatedLabel": "Last updated at",
+ "app.learningDashboard.sessionDataDownloadedLabel": "Downloaded!",
"app.learningDashboard.shareButton": "Share with others",
"app.learningDashboard.shareLinkCopied": "Link successfully copied!",
+ "app.learningDashboard.user": "Users",
"app.learningDashboard.indicators.meetingStatusEnded": "Ended",
"app.learningDashboard.indicators.meetingStatusActive": "Active",
"app.learningDashboard.indicators.usersOnline": "Active Users",
"app.learningDashboard.indicators.usersTotal": "Total Number Of Users",
"app.learningDashboard.indicators.polls": "Polls",
- "app.learningDashboard.indicators.raiseHand": "Raise Hand",
+ "app.learningDashboard.indicators.timeline": "Timeline",
"app.learningDashboard.indicators.activityScore": "Activity Score",
"app.learningDashboard.indicators.duration": "Duration",
"app.learningDashboard.usersTable.title": "Overview",
@@ -949,10 +954,17 @@
"app.learningDashboard.usersTable.userStatusOnline": "Online",
"app.learningDashboard.usersTable.userStatusOffline": "Offline",
"app.learningDashboard.usersTable.noUsers": "No users yet",
+ "app.learningDashboard.usersTable.name": "Name",
+ "app.learningDashboard.usersTable.moderator": "Moderator",
+ "app.learningDashboard.usersTable.pollVotes": "Poll Votes",
+ "app.learningDashboard.usersTable.join": "Join",
+ "app.learningDashboard.usersTable.left": "Left",
"app.learningDashboard.pollsTable.title": "Polling",
"app.learningDashboard.pollsTable.anonymousAnswer": "Anonymous Poll (answers in the last row)",
"app.learningDashboard.pollsTable.anonymousRowName": "Anonymous",
- "app.learningDashboard.statusTimelineTable.title": "Status Timeline",
+ "app.learningDashboard.pollsTable.noPollsCreatedHeading": "No polls have been created",
+ "app.learningDashboard.pollsTable.noPollsCreatedMessage": "Once a poll has been sent to users, their results will appear in this list.",
+ "app.learningDashboard.statusTimelineTable.title": "Timeline",
"app.learningDashboard.errors.invalidToken": "Invalid session token",
"app.learningDashboard.errors.dataUnavailable": "Data is no longer available"
}
diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js
index b2ca08d332..460e64c5fb 100755
--- a/bigbluebutton-html5/server/main.js
+++ b/bigbluebutton-html5/server/main.js
@@ -12,6 +12,7 @@ import '/imports/api/presentation-pods/server';
import '/imports/api/presentation-upload-token/server';
import '/imports/api/slides/server';
import '/imports/api/breakouts/server';
+import '/imports/api/breakouts-history/server';
import '/imports/api/group-chat/server';
import '/imports/api/group-chat-msg/server';
import '/imports/api/screenshare/server';
diff --git a/record-and-playback/core/lib/recordandplayback/generators/events.rb b/record-and-playback/core/lib/recordandplayback/generators/events.rb
index 855038e612..9ec6bde3f5 100755
--- a/record-and-playback/core/lib/recordandplayback/generators/events.rb
+++ b/record-and-playback/core/lib/recordandplayback/generators/events.rb
@@ -49,7 +49,7 @@ module BigBlueButton
BigBlueButton.logger.info("Task: Getting meeting metadata")
doc = Nokogiri::XML(File.open(events_xml))
metadata = {}
- doc.xpath("//metadata").each do |e|
+ doc.xpath("recording/metadata").each do |e|
e.keys.each do |k|
metadata[k] = e.attribute(k)
end
@@ -613,7 +613,7 @@ module BigBlueButton
def self.get_record_status_events(events_xml)
BigBlueButton.logger.info "Getting record status events"
rec_events = []
- events_xml.xpath("//event[@eventname='RecordStatusEvent']").each do |event|
+ events_xml.xpath("recording/event[@eventname='RecordStatusEvent']").each do |event|
s = { :timestamp => event['timestamp'].to_i }
rec_events << s
end
@@ -623,14 +623,14 @@ module BigBlueButton
def self.get_external_video_events(events_xml)
BigBlueButton.logger.info "Getting external video events"
external_videos_events = []
- events_xml.xpath("//event[@eventname='StartExternalVideoRecordEvent']").each do |event|
+ events_xml.xpath("recording/event[@eventname='StartExternalVideoRecordEvent']").each do |event|
s = {
:timestamp => event['timestamp'].to_i,
:external_video_url => event.at_xpath("externalVideoUrl").text
}
external_videos_events << s
end
- events_xml.xpath("//event[@eventname='StopExternalVideoRecordEvent']").each do |event|
+ events_xml.xpath("recording/event[@eventname='StopExternalVideoRecordEvent']").each do |event|
s = { :timestamp => event['timestamp'].to_i }
external_videos_events << s
end
@@ -892,4 +892,4 @@ module BigBlueButton
end
end
-end
+end
\ No newline at end of file
diff --git a/record-and-playback/core/lib/recordandplayback/generators/video.rb b/record-and-playback/core/lib/recordandplayback/generators/video.rb
index fdcf0ed049..76e8ecee2d 100755
--- a/record-and-playback/core/lib/recordandplayback/generators/video.rb
+++ b/record-and-playback/core/lib/recordandplayback/generators/video.rb
@@ -28,16 +28,17 @@ require File.expand_path('../../edl', __FILE__)
module BigBlueButton
- def BigBlueButton.process_webcam_videos(target_dir, temp_dir, meeting_id, output_width, output_height, output_framerate, audio_offset, processed_audio_file, video_formats=['webm'])
+ def BigBlueButton.process_webcam_videos(target_dir, raw_archive_dir, output_width, output_height, output_framerate, audio_offset, processed_audio_file, video_formats=['webm'])
BigBlueButton.logger.info("Processing webcam videos")
- events = Nokogiri::XML(File.open("#{temp_dir}/#{meeting_id}/events.xml"))
+ # raw_archive_dir already contains meeting_id
+ events = Nokogiri::XML(File.open("#{raw_archive_dir}/events.xml"))
# Process user video (camera)
start_time = BigBlueButton::Events.first_event_timestamp(events)
end_time = BigBlueButton::Events.last_event_timestamp(events)
webcam_edl = BigBlueButton::Events.create_webcam_edl(
- events, "#{temp_dir}/#{meeting_id}")
+ events, raw_archive_dir)
user_video_edl = BigBlueButton::Events.edl_match_recording_marks_video(
webcam_edl, events, start_time, end_time)
BigBlueButton::EDL::Video.dump(user_video_edl)
@@ -91,15 +92,16 @@ module BigBlueButton
end
end
- def BigBlueButton.process_deskshare_videos(target_dir, temp_dir, meeting_id, output_width, output_height, output_framerate, video_formats=['webm'])
+ def BigBlueButton.process_deskshare_videos(target_dir, raw_archive_dir, output_width, output_height, output_framerate, video_formats=['webm'])
BigBlueButton.logger.info("Processing deskshare videos")
- events = Nokogiri::XML(File.open("#{temp_dir}/#{meeting_id}/events.xml"))
+ # raw_archive_dir already contains meeting_id
+ events = Nokogiri::XML(File.open("#{raw_archive_dir}/events.xml"))
start_time = BigBlueButton::Events.first_event_timestamp(events)
end_time = BigBlueButton::Events.last_event_timestamp(events)
deskshare_edl = BigBlueButton::Events.create_deskshare_edl(
- events, "#{temp_dir}/#{meeting_id}")
+ events, raw_archive_dir)
deskshare_video_edl = BigBlueButton::Events.edl_match_recording_marks_video(
deskshare_edl, events, start_time, end_time)
@@ -167,6 +169,4 @@ module BigBlueButton
return false
end
-end
-
-
+end
\ No newline at end of file
diff --git a/record-and-playback/presentation/scripts/process/presentation.rb b/record-and-playback/presentation/scripts/process/presentation.rb
index 5c3a7ab0ad..9b2afbbcd5 100755
--- a/record-and-playback/presentation/scripts/process/presentation.rb
+++ b/record-and-playback/presentation/scripts/process/presentation.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
# Set encoding to utf-8
# encoding: UTF-8
@@ -31,15 +32,15 @@ require 'trollop'
require 'yaml'
require 'json'
-opts = Trollop::options do
- opt :meeting_id, "Meeting id to archive", :default => '58f4a6b3-cd07-444d-8564-59116cb53974', :type => String
+opts = Trollop.options do
+ opt :meeting_id, 'Meeting id to archive', default: '58f4a6b3-cd07-444d-8564-59116cb53974', type: String
end
meeting_id = opts[:meeting_id]
# This script lives in scripts/archive/steps while properties.yaml lives in scripts/
props = BigBlueButton.read_props
-presentation_props = YAML::load(File.open('presentation.yml'))
+presentation_props = YAML.safe_load(File.open('presentation.yml'))
presentation_props['audio_offset'] = 0 if presentation_props['audio_offset'].nil?
presentation_props['include_deskshare'] = false if presentation_props['include_deskshare'].nil?
@@ -48,41 +49,38 @@ raw_archive_dir = "#{recording_dir}/raw/#{meeting_id}"
log_dir = props['log_dir']
target_dir = "#{recording_dir}/process/presentation/#{meeting_id}"
-if not FileTest.directory?(target_dir)
+unless FileTest.directory?(target_dir)
FileUtils.mkdir_p "#{log_dir}/presentation"
- logger = Logger.new("#{log_dir}/presentation/process-#{meeting_id}.log", 'daily' )
+ logger = Logger.new("#{log_dir}/presentation/process-#{meeting_id}.log", 'daily')
BigBlueButton.logger = logger
- BigBlueButton.logger.info("Processing script presentation.rb")
+ BigBlueButton.logger.info('Processing script presentation.rb')
FileUtils.mkdir_p target_dir
begin
- # Create a copy of the raw archives
- temp_dir = "#{target_dir}/temp"
- FileUtils.mkdir_p temp_dir
- FileUtils.cp_r(raw_archive_dir, temp_dir)
-
# Create initial metadata.xml
- b = Builder::XmlMarkup.new(:indent => 2)
- metaxml = b.recording {
+ b = Builder::XmlMarkup.new(indent: 2)
+ metaxml = b.recording do
b.id(meeting_id)
- b.state("processing")
+ b.state('processing')
b.published(false)
b.start_time
b.end_time
b.participants
b.playback
b.meta
- }
- metadata_xml = File.new("#{target_dir}/metadata.xml","w")
+ end
+ metadata_xml = File.new("#{target_dir}/metadata.xml", 'w')
metadata_xml.write(metaxml)
metadata_xml.close
- BigBlueButton.logger.info("Created inital metadata.xml")
+ BigBlueButton.logger.info('Created inital metadata.xml')
- BigBlueButton::AudioProcessor.process("#{temp_dir}/#{meeting_id}", "#{target_dir}/audio")
- events_xml = "#{temp_dir}/#{meeting_id}/events.xml"
+ BigBlueButton::AudioProcessor.process(raw_archive_dir, "#{target_dir}/audio")
+ events_xml = "#{raw_archive_dir}/events.xml"
+
+ # TODO: Don't copy events.xml to target directory
FileUtils.cp(events_xml, target_dir)
- presentation_dir = "#{temp_dir}/#{meeting_id}/presentation"
+ presentation_dir = "#{raw_archive_dir}/presentation"
presentations = BigBlueButton::Presentation.get_presentations(events_xml)
processed_pres_dir = "#{target_dir}/presentation"
@@ -91,13 +89,11 @@ if not FileTest.directory?(target_dir)
# Get the real-time start and end timestamp
@doc = Nokogiri::XML(File.read("#{target_dir}/events.xml"))
- meeting_start = @doc.xpath("//event")[0][:timestamp]
- meeting_end = @doc.xpath("//event").last()[:timestamp]
-
+ meeting_start = BigBlueButton::Events.first_event_timestamp(@doc)
+ meeting_end = BigBlueButton::Events.last_event_timestamp(@doc)
match = /.*-(\d+)$/.match(meeting_id)
- real_start_time = match[1]
- real_end_time = (real_start_time.to_i + (meeting_end.to_i - meeting_start.to_i)).to_s
-
+ real_start_time = match[1].to_i
+ real_end_time = real_start_time + (meeting_end - meeting_start)
# Add start_time, end_time and meta to metadata.xml
## Load metadata.xml
@@ -105,48 +101,41 @@ if not FileTest.directory?(target_dir)
## Add start_time and end_time
recording = metadata.root
### Date Format for recordings: Thu Mar 04 14:05:56 UTC 2010
- start_time = recording.at_xpath("start_time")
+ start_time = recording.at_xpath('start_time')
start_time.content = real_start_time
- end_time = recording.at_xpath("end_time")
+ end_time = recording.at_xpath('end_time')
end_time.content = real_end_time
## Copy the breakout and breakout rooms node from
## events.xml if present.
- breakout_xpath = @doc.xpath("//breakout")
- breakout_rooms_xpath = @doc.xpath("//breakoutRooms")
- meeting_xpath = @doc.xpath("//meeting")
+ breakout_xpath = @doc.xpath('recording/breakout')
+ breakout_rooms_xpath = @doc.xpath('recording/breakoutRooms')
+ meeting_xpath = @doc.xpath('recording/meeting')
- if (meeting_xpath != nil)
- recording << meeting_xpath
- end
+ recording << meeting_xpath unless meeting_xpath.nil?
- if (breakout_xpath != nil)
- recording << breakout_xpath
- end
+ recording << breakout_xpath unless breakout_xpath.nil?
- if (breakout_rooms_xpath != nil)
- recording << breakout_rooms_xpath
- end
+ recording << breakout_rooms_xpath unless breakout_rooms_xpath.nil?
- participants = recording.at_xpath("participants")
+ participants = recording.at_xpath('participants')
participants.content = BigBlueButton::Events.get_num_participants(@doc)
## Remove empty meta
- metadata.search('//recording/meta').each do |meta|
- meta.remove
- end
+ ## TODO: Clarify reasoning behind creating an empty node to then remove it
+ metadata.search('recording/meta').each(&:remove)
## Add the actual meta
- metadata_with_playback = Nokogiri::XML::Builder.with(metadata.at('recording')) do |xml|
- xml.meta {
- BigBlueButton::Events.get_meeting_metadata("#{target_dir}/events.xml").each { |k,v| xml.method_missing(k,v) }
- }
+ Nokogiri::XML::Builder.with(metadata.at('recording')) do |xml|
+ xml.meta do
+ BigBlueButton::Events.get_meeting_metadata("#{target_dir}/events.xml").each { |k, v| xml.method_missing(k, v) }
+ end
end
## Write the new metadata.xml
- metadata_file = File.new("#{target_dir}/metadata.xml","w")
- metadata = Nokogiri::XML(metadata.to_xml) { |x| x.noblanks }
+ metadata_file = File.new("#{target_dir}/metadata.xml", 'w')
+ metadata = Nokogiri::XML(metadata.to_xml, &:noblanks)
metadata_file.write(metadata.root)
metadata_file.close
- BigBlueButton.logger.info("Created an updated metadata.xml with start_time and end_time")
+ BigBlueButton.logger.info('Created an updated metadata.xml with start_time and end_time')
# Start processing raw files
presentation_text = {}
@@ -158,56 +147,54 @@ if not FileTest.directory?(target_dir)
FileUtils.mkdir_p target_pres_dir
FileUtils.mkdir_p "#{target_pres_dir}/textfiles"
- images=Dir.glob("#{pres_dir}/#{pres}.{jpg,jpeg,png,gif,JPG,JPEG,PNG,GIF}")
+ images = Dir.glob("#{pres_dir}/#{pres}.{jpg,jpeg,png,gif,JPG,JPEG,PNG,GIF}")
if images.empty?
pres_name = "#{pres_dir}/#{pres}"
- if File.exists?("#{pres_name}.pdf")
+ if File.exist?("#{pres_name}.pdf")
pres_pdf = "#{pres_name}.pdf"
BigBlueButton.logger.info("Found pdf file for presentation #{pres_pdf}")
- elsif File.exists?("#{pres_name}.PDF")
+ elsif File.exist?("#{pres_name}.PDF")
pres_pdf = "#{pres_name}.PDF"
BigBlueButton.logger.info("Found PDF file for presentation #{pres_pdf}")
- elsif File.exists?("#{pres_name}")
+ elsif File.exist?(pres_name.to_s)
pres_pdf = pres_name
BigBlueButton.logger.info("Falling back to old presentation filename #{pres_pdf}")
else
- pres_pdf = ""
+ pres_pdf = ''
BigBlueButton.logger.warn("Could not find pdf file for presentation #{pres}")
end
- if !pres_pdf.empty?
+ unless pres_pdf.empty?
text = {}
1.upto(num_pages) do |page|
BigBlueButton::Presentation.extract_png_page_from_pdf(
- page, pres_pdf, "#{target_pres_dir}/slide-#{page}.png", '1600x1600')
- if File.exist?("#{pres_dir}/textfiles/slide-#{page}.txt") then
- t = File.read("#{pres_dir}/textfiles/slide-#{page}.txt", encoding: 'UTF-8')
- text["slide-#{page}"] = t.encode('UTF-8', invalid: :replace)
- FileUtils.cp("#{pres_dir}/textfiles/slide-#{page}.txt", "#{target_pres_dir}/textfiles")
- end
+ page, pres_pdf, "#{target_pres_dir}/slide-#{page}.png", '1600x1600'
+ )
+ next unless File.exist?("#{pres_dir}/textfiles/slide-#{page}.txt")
+ t = File.read("#{pres_dir}/textfiles/slide-#{page}.txt", encoding: 'UTF-8')
+ text["slide-#{page}"] = t.encode('UTF-8', invalid: :replace)
+ FileUtils.cp("#{pres_dir}/textfiles/slide-#{page}.txt", "#{target_pres_dir}/textfiles")
end
presentation_text[pres] = text
end
else
- ext = File.extname("#{images[0]}")
BigBlueButton::Presentation.convert_image_to_png(
- images[0], "#{target_pres_dir}/slide-1.png", '1600x1600')
+ images[0], "#{target_pres_dir}/slide-1.png", '1600x1600'
+ )
end
# Copy thumbnails from raw files
FileUtils.cp_r("#{pres_dir}/thumbnails", "#{target_pres_dir}/thumbnails") if File.exist?("#{pres_dir}/thumbnails")
end
- BigBlueButton.logger.info("Generating closed captions")
+ BigBlueButton.logger.info('Generating closed captions')
ret = BigBlueButton.exec_ret('utils/gen_webvtt', '-i', raw_archive_dir, '-o', target_dir)
- if ret != 0
- raise "Generating closed caption files failed"
- end
- captions = JSON.load(File.new("#{target_dir}/captions.json", 'r'))
+ raise 'Generating closed caption files failed' if ret != 0
+ captions = JSON.parse(File.read("#{target_dir}/captions.json"))
- if not presentation_text.empty?
+ unless presentation_text.empty?
# Write presentation_text.json to file
- File.open("#{target_dir}/presentation_text.json","w") { |f| f.puts presentation_text.to_json }
+ File.open("#{target_dir}/presentation_text.json", 'w') { |f| f.puts presentation_text.to_json }
end
# We have to decide whether to actually generate the webcams video file
@@ -215,40 +202,38 @@ if not FileTest.directory?(target_dir)
# - There is webcam video present, or
# - There's broadcast video present, or
# - There are closed captions present (they need a video stream to be rendered on top of)
- if !Dir["#{raw_archive_dir}/video/*"].empty? or
- !Dir["#{raw_archive_dir}/video-broadcast/*"].empty? or
- captions.length > 0
+ if !Dir["#{raw_archive_dir}/video/*"].empty? ||
+ !Dir["#{raw_archive_dir}/video-broadcast/*"].empty? ||
+ !captions.empty?
webcam_width = presentation_props['video_output_width']
webcam_height = presentation_props['video_output_height']
webcam_framerate = presentation_props['video_output_framerate']
# Use a higher resolution video canvas if there's broadcast video streams
- if !Dir["#{raw_archive_dir}/video-broadcast/*"].empty?
+ unless Dir["#{raw_archive_dir}/video-broadcast/*"].empty?
webcam_width = presentation_props['deskshare_output_width']
webcam_height = presentation_props['deskshare_output_height']
webcam_framerate = presentation_props['deskshare_output_framerate']
end
webcam_framerate = 15 if webcam_framerate.nil?
- processed_audio_file = BigBlueButton::AudioProcessor.get_processed_audio_file("#{temp_dir}/#{meeting_id}", "#{target_dir}/audio")
- BigBlueButton.process_webcam_videos(target_dir, temp_dir, meeting_id, webcam_width, webcam_height, webcam_framerate, presentation_props['audio_offset'], processed_audio_file, presentation_props['video_formats'])
+ processed_audio_file = BigBlueButton::AudioProcessor.get_processed_audio_file(raw_archive_dir, "#{target_dir}/audio")
+ BigBlueButton.process_webcam_videos(target_dir, raw_archive_dir, webcam_width, webcam_height, webcam_framerate, presentation_props['audio_offset'], processed_audio_file, presentation_props['video_formats'])
end
- if !Dir["#{raw_archive_dir}/deskshare/*"].empty? and presentation_props['include_deskshare']
+ if !Dir["#{raw_archive_dir}/deskshare/*"].empty? && presentation_props['include_deskshare']
deskshare_width = presentation_props['deskshare_output_width']
deskshare_height = presentation_props['deskshare_output_height']
deskshare_framerate = presentation_props['deskshare_output_framerate']
deskshare_framerate = 5 if deskshare_framerate.nil?
- BigBlueButton.process_deskshare_videos(target_dir, temp_dir, meeting_id, deskshare_width, deskshare_height, deskshare_framerate, presentation_props['video_formats'])
+ BigBlueButton.process_deskshare_videos(target_dir, raw_archive_dir, deskshare_width, deskshare_height, deskshare_framerate, presentation_props['video_formats'])
end
# Copy shared notes from raw files
- if !Dir["#{raw_archive_dir}/notes/*"].empty?
- FileUtils.cp_r("#{raw_archive_dir}/notes", target_dir)
- end
+ FileUtils.cp_r("#{raw_archive_dir}/notes", target_dir) unless Dir["#{raw_archive_dir}/notes/*"].empty?
- process_done = File.new("#{recording_dir}/status/processed/#{meeting_id}-presentation.done", "w")
+ process_done = File.new("#{recording_dir}/status/processed/#{meeting_id}-presentation.done", 'w')
process_done.write("Processed #{meeting_id}")
process_done.close
@@ -257,15 +242,14 @@ if not FileTest.directory?(target_dir)
metadata = Nokogiri::XML(File.read("#{target_dir}/metadata.xml"))
## Update status
recording = metadata.root
- state = recording.at_xpath("state")
- state.content = "processed"
+ state = recording.at_xpath('state')
+ state.content = 'processed'
## Write the new metadata.xml
- metadata_file = File.new("#{target_dir}/metadata.xml","w")
+ metadata_file = File.new("#{target_dir}/metadata.xml", 'w')
metadata_file.write(metadata.root)
metadata_file.close
- BigBlueButton.logger.info("Created an updated metadata.xml with state=processed")
-
- rescue Exception => e
+ BigBlueButton.logger.info('Created an updated metadata.xml with state=processed')
+ rescue StandardError => e
BigBlueButton.logger.error(e.message)
e.backtrace.each do |traceline|
BigBlueButton.logger.error(traceline)
|