[apply-toast-shared-notes] - changes in review and resolve merge conflict
This commit is contained in:
commit
491380096e
@ -89,8 +89,11 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||
|
||||
val currentPres: Option[PresentationInPod] = presentationPods.flatMap(_.getPresentation(presId)).headOption
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, userId)) {
|
||||
val reason = "No permission to export presentation."
|
||||
if (liveMeeting.props.meetingProp.disabledFeatures.contains("downloadPresentationWithAnnotations")) {
|
||||
val reason = "Annotated presentation download disabled for this meeting."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting)
|
||||
} else if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, userId)) {
|
||||
val reason = "No permission to download presentation."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting)
|
||||
} else if (currentPres.isEmpty) {
|
||||
log.error(s"Presentation ${presId} not found in meeting ${meetingId}")
|
||||
@ -126,7 +129,10 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||
val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting()
|
||||
val currentPres: Option[PresentationInPod] = presentationPods.flatMap(_.getCurrentPresentation()).headOption
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, userId)) {
|
||||
if (liveMeeting.props.meetingProp.disabledFeatures.contains("importPresentationWithAnnotationsFromBreakoutRooms")) {
|
||||
val reason = "Importing slides from breakout rooms disabled for this meeting."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting)
|
||||
} else if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, userId)) {
|
||||
val reason = "No permission to export presentation."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting)
|
||||
} else if (currentPres.isEmpty) {
|
||||
|
@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.users
|
||||
import org.bigbluebutton.LockSettingsUtil
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.running.OutMsgRouter
|
||||
import org.bigbluebutton.core.running.MeetingActor
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
@ -54,6 +55,24 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
outGW.send(notifyEvent)
|
||||
|
||||
LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW)
|
||||
|
||||
// Dial-in
|
||||
def buildLockMessage(meetingId: String, userId: String, lockedBy: String, locked: Boolean): BbbCommonEnvCoreMsg = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
|
||||
val envelope = BbbCoreEnvelope(UserLockedInMeetingEvtMsg.NAME, routing)
|
||||
val body = UserLockedInMeetingEvtMsgBody(userId, locked, lockedBy)
|
||||
val header = BbbClientMsgHeader(UserLockedInMeetingEvtMsg.NAME, meetingId, userId)
|
||||
val event = UserLockedInMeetingEvtMsg(header, body)
|
||||
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
|
||||
if (vu.intId.startsWith(IntIdPrefixType.DIAL_IN)) { // only Dial-in users need this
|
||||
val eventExplicitLock = buildLockMessage(liveMeeting.props.meetingProp.intId, vu.intId, msg.body.setBy, settings.disableMic)
|
||||
outGW.send(eventExplicitLock)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
|
18
bbb-export-annotations/.eslintrc.js
Normal file
18
bbb-export-annotations/.eslintrc.js
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
'env': {
|
||||
'commonjs': true,
|
||||
'es2021': true,
|
||||
'node': true,
|
||||
},
|
||||
'extends': [
|
||||
'google',
|
||||
],
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 'latest',
|
||||
},
|
||||
'rules': {
|
||||
'require-jsdoc': 0,
|
||||
'camelcase': 0,
|
||||
'max-len': 0,
|
||||
},
|
||||
};
|
@ -4,7 +4,11 @@
|
||||
},
|
||||
"shared": {
|
||||
"presDir": "/var/bigbluebutton",
|
||||
"presAnnDropboxDir": "/tmp/pres-ann-dropbox"
|
||||
"presAnnDropboxDir": "/tmp/pres-ann-dropbox",
|
||||
"cairosvg": "/usr/bin/cairosvg",
|
||||
"ghostscript": "/usr/bin/gs",
|
||||
"imagemagick": "/usr/bin/convert",
|
||||
"pdftocairo": "/usr/bin/pdftocairo"
|
||||
},
|
||||
"collector": {
|
||||
"pngWidthRasterizedSlides": 2560
|
||||
@ -18,14 +22,9 @@
|
||||
"notifier": {
|
||||
"pod_id": "DEFAULT_PRESENTATION_POD",
|
||||
"is_downloadable": "false",
|
||||
"msgName": "NewPresAnnFileAvailableMsg",
|
||||
"protocol": "https",
|
||||
"host": "localhost"
|
||||
},
|
||||
"bbbWeb": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 8090
|
||||
"msgName": "NewPresAnnFileAvailableMsg"
|
||||
},
|
||||
"bbbWebAPI": "http://127.0.0.1:8090",
|
||||
"redis": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 6379,
|
||||
|
@ -2,61 +2,69 @@ const Logger = require('./lib/utils/logger');
|
||||
const config = require('./config');
|
||||
const fs = require('fs');
|
||||
const redis = require('redis');
|
||||
const { commandOptions } = require('redis');
|
||||
const { Worker } = require('worker_threads');
|
||||
const {commandOptions} = require('redis');
|
||||
const {Worker} = require('worker_threads');
|
||||
const path = require('path');
|
||||
|
||||
const logger = new Logger('presAnn Master');
|
||||
logger.info("Running bbb-export-annotations");
|
||||
logger.info('Running bbb-export-annotations');
|
||||
|
||||
const kickOffCollectorWorker = (jobId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(path.join(__dirname, 'workers', 'collector.js'), { workerData: jobId });
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0)
|
||||
reject(new Error(`PresAnn Collector Worker stopped with exit code ${code}`));
|
||||
})
|
||||
})
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const collectorPath = path.join(__dirname, 'workers', 'collector.js');
|
||||
const worker = new Worker(collectorPath, {workerData: jobId});
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Collector Worker stopped with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
});
|
||||
|
||||
async function waitForJobs () {
|
||||
const queue = client.blPop(
|
||||
commandOptions({ isolated: true }),
|
||||
config.redis.channels.queue,
|
||||
0
|
||||
);
|
||||
await client.connect();
|
||||
|
||||
let job = await queue;
|
||||
|
||||
logger.info('Received job', job.element);
|
||||
const exportJob = JSON.parse(job.element);
|
||||
|
||||
// Create folder in dropbox
|
||||
let dropbox = path.join(config.shared.presAnnDropboxDir, exportJob.jobId);
|
||||
fs.mkdirSync(dropbox, { recursive: true })
|
||||
|
||||
// Drop job into dropbox as JSON
|
||||
fs.writeFile(path.join(dropbox, 'job'), job.element, function(err) {
|
||||
if(err) { return logger.error(err); }
|
||||
});
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
|
||||
kickOffCollectorWorker(exportJob.jobId)
|
||||
/**
|
||||
* Pops new export requests from a Redis queue, blocking the
|
||||
* connection otherwise.
|
||||
*/
|
||||
async function waitForJobs() {
|
||||
const queue = client.blPop(
|
||||
commandOptions({isolated: true}),
|
||||
config.redis.channels.queue,
|
||||
0,
|
||||
);
|
||||
|
||||
waitForJobs();
|
||||
}
|
||||
const job = await queue;
|
||||
|
||||
logger.info('Received job', job.element);
|
||||
const exportJob = JSON.parse(job.element);
|
||||
|
||||
// Create folder in dropbox
|
||||
const dropbox = path.join(config.shared.presAnnDropboxDir, exportJob.jobId);
|
||||
fs.mkdirSync(dropbox, {recursive: true});
|
||||
|
||||
// Drop job into dropbox as JSON
|
||||
fs.writeFile(path.join(dropbox, 'job'), job.element, function(err) {
|
||||
if (err) {
|
||||
return logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
kickOffCollectorWorker(exportJob.jobId);
|
||||
|
||||
waitForJobs();
|
||||
}
|
||||
|
||||
waitForJobs();
|
||||
})();
|
||||
|
1660
bbb-export-annotations/package-lock.json
generated
1660
bbb-export-annotations/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,8 @@
|
||||
"version": "0.0.1",
|
||||
"description": "BigBlueButton's Presentation Annotation Exporter",
|
||||
"scripts": {
|
||||
"start": "node master.js"
|
||||
"start": "node master.js",
|
||||
"lint:fix": "eslint --fix **/*.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.26.0",
|
||||
@ -13,5 +14,9 @@
|
||||
"redis": "^4.0.3",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"xmlbuilder2": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-google": "^0.14.0"
|
||||
}
|
||||
}
|
||||
|
@ -2,106 +2,101 @@ const Logger = require('../lib/utils/logger');
|
||||
const config = require('../config');
|
||||
const fs = require('fs');
|
||||
const redis = require('redis');
|
||||
const { Worker, workerData, parentPort } = require('worker_threads');
|
||||
const {Worker, workerData} = require('worker_threads');
|
||||
const path = require('path');
|
||||
const { execSync } = require("child_process");
|
||||
const cp = require('child_process');
|
||||
|
||||
const jobId = workerData;
|
||||
|
||||
const logger = new Logger('presAnn Collector');
|
||||
logger.info("Collecting job " + jobId);
|
||||
logger.info('Collecting job ' + jobId);
|
||||
|
||||
const kickOffProcessWorker = (jobId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker('./workers/process.js', { workerData: jobId });
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0)
|
||||
reject(new Error(`PresAnn Process Worker stopped with exit code ${code}`));
|
||||
})
|
||||
})
|
||||
}
|
||||
const worker = new Worker('./workers/process.js', {workerData: jobId});
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Process Worker stopped with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
|
||||
|
||||
// Takes the Job from the dropbox
|
||||
let job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
let exportJob = JSON.parse(job);
|
||||
const job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
const exportJob = JSON.parse(job);
|
||||
|
||||
// Collect the annotations from Redis
|
||||
(async () => {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password
|
||||
});
|
||||
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
let presAnn = await client.hGetAll(exportJob.jobId);
|
||||
|
||||
// Remove annotations from Redis
|
||||
await client.DEL(jobId);
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
|
||||
client.disconnect();
|
||||
await client.connect();
|
||||
|
||||
let annotations = JSON.stringify(presAnn);
|
||||
const presAnn = await client.hGetAll(exportJob.jobId);
|
||||
|
||||
let whiteboard = JSON.parse(annotations);
|
||||
let pages = JSON.parse(whiteboard.pages);
|
||||
|
||||
fs.writeFile(path.join(dropbox, 'whiteboard'), annotations, function(err) {
|
||||
if(err) { return logger.error(err); }
|
||||
});
|
||||
// Remove annotations from Redis
|
||||
await client.del(jobId);
|
||||
|
||||
// Collect the Presentation Page files (PDF / PNG / JPEG) from the presentation directory
|
||||
let presentationFile = path.join(exportJob.presLocation, exportJob.presId);
|
||||
let pdfFile = `${presentationFile}.pdf`
|
||||
client.disconnect();
|
||||
|
||||
if (fs.existsSync(pdfFile)) {
|
||||
const annotations = JSON.stringify(presAnn);
|
||||
|
||||
for (let p of pages) {
|
||||
let pageNumber = p.page;
|
||||
let outputFile = path.join(dropbox, `slide${pageNumber}`);
|
||||
|
||||
// CairoSVG doesn't handle transparent SVG and PNG embeds properly, e.g., in rasterized text.
|
||||
// So textboxes may get a black background when downloading/exporting repeatedly.
|
||||
// To avoid that, we take slides from the uploaded file, but later probe the dimensions from the SVG
|
||||
// so it matches what was shown in the browser.
|
||||
const whiteboard = JSON.parse(annotations);
|
||||
const pages = JSON.parse(whiteboard.pages);
|
||||
|
||||
let extract_png_from_pdf = [
|
||||
'pdftocairo',
|
||||
'-png',
|
||||
'-f', pageNumber,
|
||||
'-l', pageNumber,
|
||||
'-scale-to', config.collector.pngWidthRasterizedSlides,
|
||||
'-singlefile',
|
||||
'-cropbox',
|
||||
pdfFile, outputFile,
|
||||
].join(' ')
|
||||
|
||||
execSync(extract_png_from_pdf);
|
||||
}
|
||||
fs.writeFile(path.join(dropbox, 'whiteboard'), annotations, function(err) {
|
||||
if (err) {
|
||||
return logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
// If PNG file already available
|
||||
else if (fs.existsSync(`${presentationFile}.png`)) {
|
||||
fs.copyFileSync(`${presentationFile}.png`, path.join(dropbox, 'slide1.png'));
|
||||
}
|
||||
|
||||
// If JPEG file available
|
||||
else if (fs.existsSync(`${presentationFile}.jpeg`)) {
|
||||
fs.copyFileSync(`${presentationFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
|
||||
// Collect the presentation page files (PDF / PNG / JPEG)
|
||||
// from the presentation directory
|
||||
const presFile = path.join(exportJob.presLocation, exportJob.presId);
|
||||
const pdfFile = `${presFile}.pdf`;
|
||||
|
||||
if (fs.existsSync(pdfFile)) {
|
||||
for (const p of pages) {
|
||||
const pageNumber = p.page;
|
||||
const outputFile = path.join(dropbox, `slide${pageNumber}`);
|
||||
|
||||
// CairoSVG doesn't handle transparent SVG and PNG embeds properly,
|
||||
// e.g., in rasterized text. So textboxes may get a black background
|
||||
// when downloading/exporting repeatedly. To avoid that, we take slides
|
||||
// from the uploaded file, but later probe the dimensions from the SVG
|
||||
// so it matches what was shown in the browser.
|
||||
|
||||
const extract_png_from_pdf = [
|
||||
'-png',
|
||||
'-f', pageNumber,
|
||||
'-l', pageNumber,
|
||||
'-scale-to', config.collector.pngWidthRasterizedSlides,
|
||||
'-singlefile',
|
||||
'-cropbox',
|
||||
pdfFile, outputFile,
|
||||
];
|
||||
|
||||
cp.spawnSync(config.shared.pdftocairo, extract_png_from_pdf, {shell: false});
|
||||
}
|
||||
// If PNG file already available
|
||||
} else if (fs.existsSync(`${presFile}.png`)) {
|
||||
fs.copyFileSync(`${presFile}.png`, path.join(dropbox, 'slide1.png'));
|
||||
// If JPEG file available
|
||||
} else if (fs.existsSync(`${presFile}.jpeg`)) {
|
||||
fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
|
||||
} else {
|
||||
return logger.error(`Could not find presentation file ${exportJob.jobId}`);
|
||||
}
|
||||
|
||||
else {
|
||||
return logger.error(`Could not find whiteboard presentation file for job ${exportJob.jobId}`);
|
||||
}
|
||||
|
||||
kickOffProcessWorker(exportJob.jobId);
|
||||
})()
|
||||
|
||||
parentPort.postMessage({ message: workerData })
|
||||
kickOffProcessWorker(exportJob.jobId);
|
||||
})();
|
||||
|
@ -6,79 +6,87 @@ const redis = require('redis');
|
||||
const axios = require('axios').default;
|
||||
const path = require('path');
|
||||
|
||||
const { workerData, parentPort } = require('worker_threads')
|
||||
const {workerData} = require('worker_threads');
|
||||
|
||||
const [jobType, jobId, filename] = workerData;
|
||||
const [jobType, jobId, filename_with_extension] = workerData;
|
||||
|
||||
const logger = new Logger('presAnn Notifier Worker');
|
||||
|
||||
const dropbox = `${config.shared.presAnnDropboxDir}/${jobId}`
|
||||
let job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
let exportJob = JSON.parse(job);
|
||||
const dropbox = `${config.shared.presAnnDropboxDir}/${jobId}`;
|
||||
const job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
const exportJob = JSON.parse(job);
|
||||
|
||||
/** Notify Meeting Actor of file availability by
|
||||
* sending a message through Redis PubSub */
|
||||
async function notifyMeetingActor() {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password
|
||||
});
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
await client.connect();
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
|
||||
let link = `${config.notifier.protocol}://${config.notifier.host}/bigbluebutton/presentation/${exportJob.parentMeetingId}/${exportJob.parentMeetingId}/${exportJob.presId}/pdf/${jobId}/${filename}.pdf`;
|
||||
// Notify Meeting Actor of file availability by sending a message through Redis PubSub
|
||||
const notification = {
|
||||
envelope: {
|
||||
name: config.notifier.msgName,
|
||||
routing: {
|
||||
sender: exportJob.module
|
||||
},
|
||||
timestamp: (new Date()).getTime(),
|
||||
},
|
||||
core: {
|
||||
header: {
|
||||
name: config.notifier.msgName,
|
||||
meetingId: exportJob.parentMeetingId,
|
||||
userId: ""
|
||||
},
|
||||
body: {
|
||||
fileURI: link,
|
||||
presId: exportJob.presId
|
||||
},
|
||||
}
|
||||
}
|
||||
const link = path.join(`${path.sep}bigbluebutton`, 'presentation',
|
||||
exportJob.parentMeetingId, exportJob.parentMeetingId,
|
||||
exportJob.presId, 'pdf', jobId, filename_with_extension);
|
||||
|
||||
logger.info(`Annotated PDF available at ${link}`);
|
||||
await client.publish(config.redis.channels.publish, JSON.stringify(notification));
|
||||
client.disconnect();
|
||||
const notification = {
|
||||
envelope: {
|
||||
name: config.notifier.msgName,
|
||||
routing: {
|
||||
sender: exportJob.module,
|
||||
},
|
||||
timestamp: (new Date()).getTime(),
|
||||
},
|
||||
core: {
|
||||
header: {
|
||||
name: config.notifier.msgName,
|
||||
meetingId: exportJob.parentMeetingId,
|
||||
userId: '',
|
||||
},
|
||||
body: {
|
||||
fileURI: link,
|
||||
presId: exportJob.presId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
logger.info(`Annotated PDF available at ${link}`);
|
||||
await client.publish(config.redis.channels.publish,
|
||||
JSON.stringify(notification));
|
||||
client.disconnect();
|
||||
}
|
||||
|
||||
async function upload(exportJob) {
|
||||
let callbackUrl = `http://${config.bbbWeb.host}:${config.bbbWeb.port}/bigbluebutton/presentation/${exportJob.presentationUploadToken}/upload`
|
||||
let formData = new FormData();
|
||||
/** Upload PDF to a BBB room */
|
||||
async function upload() {
|
||||
const callbackUrl = `${config.bbbWebAPI}/bigbluebutton/presentation/${exportJob.presentationUploadToken}/upload`;
|
||||
const formData = new FormData();
|
||||
const file = `${exportJob.presLocation}/pdfs/${jobId}/${filename_with_extension}`;
|
||||
|
||||
formData.append('conference', exportJob.parentMeetingId);
|
||||
formData.append('pod_id', config.notifier.pod_id);
|
||||
formData.append('is_downloadable', config.notifier.is_downloadable);
|
||||
formData.append('temporaryPresentationId', jobId);
|
||||
formData.append('fileUpload', fs.createReadStream(`${exportJob.presLocation}/pdfs/${jobId}/${filename}.pdf`));
|
||||
formData.append('conference', exportJob.parentMeetingId);
|
||||
formData.append('pod_id', config.notifier.pod_id);
|
||||
formData.append('is_downloadable', config.notifier.is_downloadable);
|
||||
formData.append('temporaryPresentationId', jobId);
|
||||
formData.append('fileUpload', fs.createReadStream(file));
|
||||
|
||||
let res = await axios.post(callbackUrl, formData, { headers: formData.getHeaders() });
|
||||
logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`);
|
||||
const res = await axios.post(callbackUrl, formData,
|
||||
{headers: formData.getHeaders()});
|
||||
logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`);
|
||||
}
|
||||
|
||||
if (jobType == 'PresentationWithAnnotationDownloadJob') {
|
||||
notifyMeetingActor();
|
||||
|
||||
notifyMeetingActor();
|
||||
} else if (jobType == 'PresentationWithAnnotationExportJob') {
|
||||
upload(exportJob);
|
||||
|
||||
upload();
|
||||
} else {
|
||||
logger.error(`Notifier received unknown job type ${jobType}`);
|
||||
logger.error(`Notifier received unknown job type ${jobType}`);
|
||||
}
|
||||
|
||||
// Delete temporary files
|
||||
fs.rm(dropbox, { recursive: true }, (err) => { if (err) { throw err; } });
|
||||
|
||||
parentPort.postMessage({ message: workerData })
|
||||
fs.rm(dropbox, {recursive: true}, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
FormattedMessage, FormattedNumber, FormattedTime, injectIntl,
|
||||
@ -6,6 +6,7 @@ import {
|
||||
import { UserDetailsContext } from './context';
|
||||
import UserAvatar from '../UserAvatar';
|
||||
import { getSumOfTime, tsToHHmmss, getActivityScore } from '../../services/UserService';
|
||||
import { usePreviousValue } from '../../utils/hooks';
|
||||
import { toCamelCase } from '../../utils/string';
|
||||
|
||||
const UserDatailsComponent = (props) => {
|
||||
@ -15,15 +16,36 @@ const UserDatailsComponent = (props) => {
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const modal = useRef();
|
||||
const closeButton = useRef();
|
||||
const wasModalOpen = usePreviousValue(isOpen);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e) => {
|
||||
const keydownhandler = (e) => {
|
||||
if (e.code === 'Escape') dispatch({ type: 'closeModal' });
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
const focusHandler = () => {
|
||||
if (modal.current && document.activeElement) {
|
||||
if (!modal.current.contains(document.activeElement)) {
|
||||
closeButton.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', keydownhandler);
|
||||
window.addEventListener('focus', focusHandler, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', keydownhandler);
|
||||
window.removeEventListener('focus', focusHandler, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wasModalOpen) closeButton.current?.focus();
|
||||
});
|
||||
|
||||
const {
|
||||
createdOn, endedOn, polls, users,
|
||||
} = dataJson;
|
||||
@ -288,12 +310,14 @@ const UserDatailsComponent = (props) => {
|
||||
role="none"
|
||||
onClick={() => dispatch({ type: 'closeModal' })}
|
||||
/>
|
||||
<div className="overflow-auto w-full md:w-2/4 bg-gray-100 p-6">
|
||||
<div ref={modal} className="overflow-auto w-full md:w-2/4 bg-gray-100 p-6">
|
||||
<div className="text-right rtl:text-left">
|
||||
<button
|
||||
onClick={() => dispatch({ type: 'closeModal' })}
|
||||
type="button"
|
||||
aria-label="Close user details modal"
|
||||
ref={closeButton}
|
||||
className="focus:rounded-md focus:outline-none focus:ring focus:ring-gray-500 focus:ring-opacity-50 hover:text-black/50 active:text-black/75"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
|
19
bbb-learning-dashboard/src/utils/hooks/index.js
Normal file
19
bbb-learning-dashboard/src/utils/hooks/index.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* Custom hook to get previous values. It can be used, for example,
|
||||
* to retrieve previous props or state.
|
||||
* @param {*} value
|
||||
* @returns The previous value.
|
||||
*/
|
||||
export const usePreviousValue = (value) => {
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
};
|
||||
|
||||
export default {
|
||||
usePreviousValue,
|
||||
};
|
@ -94,35 +94,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
color: var(--palette-placeholder-text);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#TD-PrimaryTools {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#TD-Tools {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
bottom: 5rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] #TD-Tools {
|
||||
left: 1rem;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
#TD-Tools > div {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#TD-Styles + div {
|
||||
display: flex;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] #TD-Styles + div {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 0px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('gesturestart', function (e) {
|
||||
|
@ -5,7 +5,7 @@ import Logger from '/imports/startup/server/logger';
|
||||
import setPresentationExporting from '/imports/api/presentations/server/modifiers/setPresentationExporting';
|
||||
import Presentations from '/imports/api/presentations';
|
||||
|
||||
const EXPORTING_THRESHOLD_PER_SLIDE = 60000;
|
||||
const EXPORTING_THRESHOLD_PER_SLIDE = 2500;
|
||||
|
||||
export default function exportPresentationToChat(presentationId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
|
@ -29,6 +29,8 @@ const ActionsBarContainer = (props) => {
|
||||
|
||||
const amIPresenter = users[Auth.meetingID][Auth.userID].presenter;
|
||||
|
||||
if (actionsBarStyle.display === false) return null;
|
||||
|
||||
return (
|
||||
<ActionsBar {
|
||||
...{
|
||||
|
@ -456,13 +456,12 @@ class AudioModal extends Component {
|
||||
<Styled.AudioDial
|
||||
label={dialAudioLabel}
|
||||
size="md"
|
||||
color="primary"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
content: 'audioDial',
|
||||
});
|
||||
}}
|
||||
ghost
|
||||
/>
|
||||
) : null}
|
||||
<CaptionsSelectContainer />
|
||||
|
@ -9,6 +9,9 @@ const ClosedCaptionToggleButton = styled(Button)`
|
||||
background-color: transparent !important;
|
||||
border-color: ${colorWhite} !important;
|
||||
}
|
||||
i {
|
||||
margin-top: .4rem;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
|
@ -53,6 +53,7 @@ const Chat = (props) => {
|
||||
syncing,
|
||||
syncedPercent,
|
||||
lastTimeWindowValuesBuild,
|
||||
width,
|
||||
} = props;
|
||||
|
||||
const userSentMessage = UserSentMessageCollection.findOne({ userId: Auth.userID, sent: true });
|
||||
@ -136,6 +137,7 @@ const Chat = (props) => {
|
||||
syncedPercent,
|
||||
lastTimeWindowValuesBuild,
|
||||
userSentMessage,
|
||||
width,
|
||||
}}
|
||||
/>
|
||||
<MessageFormContainer
|
||||
|
@ -7,6 +7,7 @@ import { AutoSizer,CellMeasurer, CellMeasurerCache } from 'react-virtualized';
|
||||
import Styled from './styles';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import TimeWindowChatItem from './time-window-chat-item/container';
|
||||
import { convertRemToPixels } from '/imports/utils/dom-utils';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
@ -271,6 +272,7 @@ class TimeWindowList extends PureComponent {
|
||||
render() {
|
||||
const {
|
||||
timeWindowsValues,
|
||||
width,
|
||||
} = this.props;
|
||||
const {
|
||||
scrollArea,
|
||||
@ -285,6 +287,8 @@ class TimeWindowList extends PureComponent {
|
||||
&& !userScrolledBack
|
||||
);
|
||||
|
||||
const paddingValue = convertRemToPixels(2);
|
||||
|
||||
return (
|
||||
[
|
||||
<Styled.MessageListWrapper
|
||||
@ -306,8 +310,8 @@ class TimeWindowList extends PureComponent {
|
||||
aria-live="polite"
|
||||
ref={node => this.messageListWrapper = node}
|
||||
>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => {
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => {
|
||||
if (width !== this.lastWidth) {
|
||||
this.lastWidth = width;
|
||||
this.cache.clearAll();
|
||||
@ -328,7 +332,7 @@ class TimeWindowList extends PureComponent {
|
||||
rowRenderer={this.rowRender}
|
||||
rowCount={timeWindowsValues.length}
|
||||
height={height}
|
||||
width={width}
|
||||
width={width - paddingValue}
|
||||
overscanRowCount={0}
|
||||
deferredMeasurementCache={this.cache}
|
||||
scrollToIndex={shouldAutoScroll ? scrollPosition : undefined}
|
||||
|
@ -4,6 +4,8 @@ import { injectIntl } from 'react-intl';
|
||||
import { Picker } from 'emoji-mart';
|
||||
import 'emoji-mart/css/emoji-mart.css';
|
||||
|
||||
const DISABLE_EMOJIS = Meteor.settings.public.chat.disableEmojis;
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
@ -21,42 +23,7 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
const emojisToExclude = [
|
||||
// People & Body
|
||||
'1F971', '1F90F', '1F9BE', '1F9BF', '1F9BB', '1F9D1-200D-1F9B0', '1F9D1-200D-1F9B1',
|
||||
'1F9D1-200D-1F9B3', '1F9D1-200D-1F9B2', '1F9CF', '1F9CF-200D-2642-FE0F',
|
||||
'1F9CF-200D-2640-FE0F', '1F9D1-200D-2695-FE0F', '1F9D1-200D-1F393', '1F9D1-200D-1F3EB',
|
||||
'1F9D1-200D-2696-FE0F', '1F9D1-200D-1F33E', '1F9D1-200D-1F373', '1F9D1-200D-1F527',
|
||||
'1F9D1-200D-1F3ED', '1F9D1-200D-1F4BC', '1F9D1-200D-1F52C', '1F9D1-200D-1F4BB',
|
||||
'1F9D1-200D-1F3A4', '1F9D1-200D-1F3A8', '1F9D1-200D-2708-FE0F', '1F9D1-200D-1F680',
|
||||
'1F9D1-200D-1F692', '1F9CD', '1F9CD-200D-2642-FE0F', '1F9CD-200D-2640-FE0F',
|
||||
'1F9CE', '1F9CE-200D-2642-FE0F', '1F9CE-200D-2640-FE0F', '1F9D1-200D-1F91D-200D-1F9D1',
|
||||
'1F90E', '1F90D', '1F469-200D-1F9B0', '1F468-200D-1F9B2', '1F469-200D-1F9B1',
|
||||
'1F469-200D-1F9B2', '1F970', '263A-FE0F', '1F975', '1F976', '1F973', '1F97A', '1F9B5',
|
||||
'1F9B6', '1F9B7', '1F9B4', '1F974', '1F468-200D-1F9B1', '1F468-200D-1F9B3',
|
||||
'1F469-200D-1F9B0', '1F469-200D-1F9B3', '1F9B8', '1F9B8-200D-2642-FE0F',
|
||||
'1F9B8-200D-2640-FE0F', '1F9B9', '1F9B9-200D-2642-FE0F', '1F9B9-200D-2640-FE0F',
|
||||
'1F9D1-200D-1F9AF', '1F468-200D-1F9AF', '1F469-200D-1F9AF', '1F9D1-200D-1F9BC',
|
||||
'1F468-200D-1F9BC', '1F469-200D-1F9BC', '1F9D1-200D-1F9BD', '1F468-200D-1F9BD',
|
||||
'1F469-200D-1F9BD', '1F468-200D-1F9B0', '1F595', '1F92E',
|
||||
// Animals & Nature
|
||||
'1F9A7', '1F9AE', '1F9A5', '1F9A6', '1F9A8', '1F9A9', '1F415-200D-1F9BA', '1F99D',
|
||||
'1F999', '1F99B', '1F998', '1F9A1', '1F9A2', '1F99A', '1F99C', '1F99F', '1F9A0',
|
||||
// Food & Drink
|
||||
'1F9C4', '1F9C5', '1F9C7', '1F9C6', '1F9C8', '1F9AA', '1F9C3', '1F9C9', '1F9CA', '1F96D',
|
||||
'1F96C', '1F96F', '1F9C2', '1F96E', '1F99E', '1F9C1',
|
||||
// Activity
|
||||
'1F93F', '1FA80', '1FA81', '1F9E8', '1F9E7', '1F94E', '1F94F', '1F94D', '1F9FF', '1F9E9',
|
||||
'1F9F8', '1F9F5', '1F9F6',
|
||||
// Travel & Places
|
||||
'1F6D5', '1F9BD', '1F9BC', '1F6FA', '1FA82', '1FA90', '1F9ED', '1F9F1', '1F6F9', '1F9F3',
|
||||
// Objects
|
||||
'1F9BA', '1F97B', '1FA71', '1FA72', '1FA73', '1FA70', '1FA95', '1FA94', '1FA93', '1F9AF',
|
||||
'1FA78', '1FA79', '1FA7A', '1FA91', '1FA92', '1F97D', '1F97C', '1F97E', '1F97F', '1F9EE',
|
||||
'1F9FE', '1F9F0', '1F9F2', '1F9EA', '1F9EB', '1F9EC', '1F9F4', '1F9F7', '1F9F9',
|
||||
'1F9FA', '1F9FB', '1F9FC', '1F9FD', '1F9EF',
|
||||
// Symbols
|
||||
'1F7E0', '1F7E1', '1F7E2', '1F7E3', '1F7E4', '1F7E5', '1F7E7', '1F7E8', '1F7E9',
|
||||
'1F7E6', '1F7EA', '1F7EB',
|
||||
...DISABLE_EMOJIS,
|
||||
];
|
||||
|
||||
const EmojiPicker = (props) => {
|
||||
|
@ -99,7 +99,7 @@ export const INITIAL_INPUT_STATE = {
|
||||
|
||||
export const INITIAL_OUTPUT_STATE = {
|
||||
navBar: {
|
||||
display: true,
|
||||
display: false,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
@ -108,7 +108,7 @@ export const INITIAL_OUTPUT_STATE = {
|
||||
zIndex: 1,
|
||||
},
|
||||
actionBar: {
|
||||
display: true,
|
||||
display: false,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
|
@ -57,7 +57,7 @@ const NavBarContainer = ({ children, ...props }) => {
|
||||
|
||||
const hideNavBar = getFromUserSettings('bbb_hide_nav_bar', false);
|
||||
|
||||
if (hideNavBar) return null;
|
||||
if (hideNavBar || navBar.display === false) return null;
|
||||
|
||||
return (
|
||||
<NavBar
|
||||
|
@ -169,7 +169,6 @@ class Presentation extends PureComponent {
|
||||
} = this.props;
|
||||
|
||||
const { presentationWidth, presentationHeight } = this.state;
|
||||
|
||||
const {
|
||||
numCameras: prevNumCameras,
|
||||
presentationBounds: prevPresentationBounds,
|
||||
|
@ -272,13 +272,8 @@ export const ToastController = ({ intl }) => {
|
||||
const convertingPresentations = presentationsRenderedFalseAndConversionFalse.filter(p => !p.renderedInToast )
|
||||
let tmpIdconvertingPresentations = presentationsRenderedFalseAndConversionFalse.filter(p => !p.conversion.done)
|
||||
.map(p => p.tmpPresId)
|
||||
// tmpIdconvertingPresentations = UploadingPresentations.find({}).fetch().filter(p => tmpIdconvertingPresentations.includes(p.tmpPresId))
|
||||
// .map(p => p.tmpPresId)
|
||||
UploadingPresentations.find({}).fetch().filter(p => tmpIdconvertingPresentations.includes(p.tmpPresId))
|
||||
.map(p => UploadingPresentations.remove({tmpPresId: p.tmpPresId}));
|
||||
// Remove all presentations from the uploading collection if they are already
|
||||
// converting.
|
||||
// UploadingPresentations.remove({tmpPresId: {$all: tmpIdconvertingPresentations}});
|
||||
const uploadingPresentations = UploadingPresentations.find().fetch();
|
||||
let presentationsToConvert = convertingPresentations.concat(uploadingPresentations);
|
||||
|
||||
|
@ -695,7 +695,7 @@ class PresentationUploader extends Component {
|
||||
|
||||
renderPresentationList() {
|
||||
const { presentations } = this.state;
|
||||
const { intl } = this.props;
|
||||
const { intl, allowDownloadable } = this.props;
|
||||
|
||||
let presentationsSorted = presentations;
|
||||
|
||||
@ -737,7 +737,9 @@ class PresentationUploader extends Component {
|
||||
</tr>
|
||||
<Styled.Head>
|
||||
<th colSpan={4}>{intl.formatMessage(intlMessages.currentLabel)}</th>
|
||||
<th>{intl.formatMessage(intlMessages.downloadLabel)}</th>
|
||||
{
|
||||
allowDownloadable ? <th>{intl.formatMessage(intlMessages.downloadLabel)}</th> : null
|
||||
}
|
||||
</Styled.Head>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -8,6 +8,7 @@ import PresUploaderController from '/imports/ui/components/presentation/presenta
|
||||
import PresentationUploader from './component';
|
||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { isDownloadPresentationWithAnnotationsEnabled } from '/imports/ui/services/features';
|
||||
|
||||
const PRESENTATION_CONFIG = Meteor.settings.public.presentation;
|
||||
|
||||
@ -39,7 +40,7 @@ export default withTracker(() => {
|
||||
fileSizeMax: PRESENTATION_CONFIG.mirroredFromBBBCore.uploadSizeMax,
|
||||
filePagesMax: PRESENTATION_CONFIG.mirroredFromBBBCore.uploadPagesMax,
|
||||
fileValidMimeTypes: PRESENTATION_CONFIG.uploadValidMimeTypes,
|
||||
allowDownloadable: PRESENTATION_CONFIG.allowDownloadable,
|
||||
allowDownloadable: isDownloadPresentationWithAnnotationsEnabled(),
|
||||
handleSave: Service.handleSavePresentation,
|
||||
handleDismissToast: PresUploaderController.handleDismissToast,
|
||||
renderToastList: Service.renderToastList,
|
||||
|
@ -78,8 +78,6 @@ const observePresentationConversion = (
|
||||
onConversion,
|
||||
) => new Promise((resolve) => {
|
||||
|
||||
|
||||
// Fazer o onConversion modificar a upload e ver o que mais da pra fazer
|
||||
const conversionTimeout = setTimeout(() => {
|
||||
onConversion({
|
||||
done: true,
|
||||
|
@ -6,6 +6,7 @@ import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import Modal from '/imports/ui/components/common/modal/simple/component';
|
||||
import _ from 'lodash';
|
||||
import Styled from './styles';
|
||||
import StyledSettings from '../settings/styles';
|
||||
import withShortcutHelper from './service';
|
||||
import { isChatEnabled } from '/imports/ui/services/features';
|
||||
|
||||
@ -114,12 +115,151 @@ const intlMessages = defineMessages({
|
||||
id: 'app.shortcut-help.previousSlideKey',
|
||||
description: 'describes the previous slide shortcut key',
|
||||
},
|
||||
select: {
|
||||
id: 'app.shortcut-help.select',
|
||||
description: 'describes the selection tool shortcut key',
|
||||
},
|
||||
pencil: {
|
||||
id: 'app.shortcut-help.pencil',
|
||||
description: 'describes the pencil tool shortcut key',
|
||||
},
|
||||
eraser: {
|
||||
id: 'app.shortcut-help.eraser',
|
||||
description: 'describes the eraser tool shortcut key',
|
||||
},
|
||||
rectangle: {
|
||||
id: 'app.shortcut-help.rectangle',
|
||||
description: 'describes the rectangle shape tool shortcut key',
|
||||
},
|
||||
elipse: {
|
||||
id: 'app.shortcut-help.elipse',
|
||||
description: 'describes the elipse shape tool shortcut key',
|
||||
},
|
||||
triangle: {
|
||||
id: 'app.shortcut-help.triangle',
|
||||
description: 'describes the triangle shape tool shortcut key',
|
||||
},
|
||||
line: {
|
||||
id: 'app.shortcut-help.line',
|
||||
description: 'describes the line shape tool shortcut key',
|
||||
},
|
||||
arrow: {
|
||||
id: 'app.shortcut-help.arrow',
|
||||
description: 'describes the arrow shape tool shortcut key',
|
||||
},
|
||||
text: {
|
||||
id: 'app.shortcut-help.text',
|
||||
description: 'describes the text tool shortcut key',
|
||||
},
|
||||
note: {
|
||||
id: 'app.shortcut-help.note',
|
||||
description: 'describes the sticky note shortcut key',
|
||||
},
|
||||
general: {
|
||||
id: 'app.shortcut-help.general',
|
||||
description: 'general tab heading',
|
||||
},
|
||||
presentation: {
|
||||
id: 'app.shortcut-help.presentation',
|
||||
description: 'presentation tab heading',
|
||||
},
|
||||
whiteboard: {
|
||||
id: 'app.shortcut-help.whiteboard',
|
||||
description: 'whiteboard tab heading',
|
||||
},
|
||||
zoomIn: {
|
||||
id: 'app.shortcut-help.zoomIn',
|
||||
description: 'describes the zoom in shortcut key',
|
||||
},
|
||||
zoomOut: {
|
||||
id: 'app.shortcut-help.zoomOut',
|
||||
description: 'describes the zoom out shortcut key',
|
||||
},
|
||||
zoomFit: {
|
||||
id: 'app.shortcut-help.zoomFit',
|
||||
description: 'describes the zoom to fit shortcut key',
|
||||
},
|
||||
zoomSelect: {
|
||||
id: 'app.shortcut-help.zoomSelect',
|
||||
description: 'describes the zoom to selection shortcut key',
|
||||
},
|
||||
flipH: {
|
||||
id: 'app.shortcut-help.flipH',
|
||||
description: 'describes the flip horozontally shortcut key',
|
||||
},
|
||||
flipV: {
|
||||
id: 'app.shortcut-help.flipV',
|
||||
description: 'describes the flip vertically shortcut key',
|
||||
},
|
||||
lock: {
|
||||
id: 'app.shortcut-help.lock',
|
||||
description: 'describes the lock / unlock shape shortcut key',
|
||||
},
|
||||
moveToFront: {
|
||||
id: 'app.shortcut-help.moveToFront',
|
||||
description: 'describes the move to front shortcut key',
|
||||
},
|
||||
moveToBack: {
|
||||
id: 'app.shortcut-help.moveToBack',
|
||||
description: 'describes the move to back shortcut key',
|
||||
},
|
||||
moveForward: {
|
||||
id: 'app.shortcut-help.moveForward',
|
||||
description: 'describes the move forward shortcut key',
|
||||
},
|
||||
moveBackward: {
|
||||
id: 'app.shortcut-help.moveBackward',
|
||||
description: 'describes the move backward shortcut key',
|
||||
},
|
||||
undo: {
|
||||
id: 'app.shortcut-help.undo',
|
||||
description: 'describes the undo shortcut key',
|
||||
},
|
||||
redo: {
|
||||
id: 'app.shortcut-help.redo',
|
||||
description: 'describes the redo shortcut key',
|
||||
},
|
||||
cut: {
|
||||
id: 'app.shortcut-help.cut',
|
||||
description: 'describes the cut shortcut key',
|
||||
},
|
||||
copy: {
|
||||
id: 'app.shortcut-help.copy',
|
||||
description: 'describes the cut shortcut key',
|
||||
},
|
||||
paste: {
|
||||
id: 'app.shortcut-help.paste',
|
||||
description: 'describes the paste shortcut key',
|
||||
},
|
||||
selectAll: {
|
||||
id: 'app.shortcut-help.selectAll',
|
||||
description: 'describes the select all shortcut key',
|
||||
},
|
||||
delete: {
|
||||
id: 'app.shortcut-help.delete',
|
||||
description: 'describes the delete shortcut key',
|
||||
},
|
||||
duplicate: {
|
||||
id: 'app.shortcut-help.duplicate',
|
||||
description: 'describes the duplicate shortcut key',
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const renderItem = (func, key) => {
|
||||
return (
|
||||
<tr key={_.uniqueId('hotkey-item-')}>
|
||||
<Styled.DescCell>{func}</Styled.DescCell>
|
||||
<Styled.KeyCell>{key}</Styled.KeyCell>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
const ShortcutHelpComponent = (props) => {
|
||||
const { intl, shortcuts } = props;
|
||||
const { browserName } = browserInfo;
|
||||
const { isIos, isMacos } = deviceInfo;
|
||||
const [ selectedTab, setSelectedTab] = React.useState(0);
|
||||
|
||||
let accessMod = null;
|
||||
|
||||
@ -147,43 +287,54 @@ const ShortcutHelpComponent = (props) => {
|
||||
accessMod = 'Control + Option';
|
||||
}
|
||||
|
||||
const shortcutItems = shortcuts.map((shortcut) => {
|
||||
const generalShortcutItems = shortcuts.map((shortcut) => {
|
||||
if (!isChatEnabled() && shortcut.descId.indexOf('Chat') !== -1) return null;
|
||||
return (
|
||||
<tr key={_.uniqueId('hotkey-item-')}>
|
||||
<Styled.KeyCell>{`${accessMod} + ${shortcut.accesskey}`}</Styled.KeyCell>
|
||||
<Styled.DescCell>{`${intl.formatMessage(intlMessages[`${shortcut.descId.toLowerCase()}`])}`}</Styled.DescCell>
|
||||
</tr>
|
||||
return renderItem(
|
||||
`${intl.formatMessage(intlMessages[`${shortcut.descId.toLowerCase()}`])}`,
|
||||
`${accessMod} + ${shortcut.accesskey}`
|
||||
);
|
||||
});
|
||||
|
||||
shortcutItems.push((
|
||||
<tr key={_.uniqueId('hotkey-item-')}>
|
||||
<Styled.KeyCell>{intl.formatMessage(intlMessages.togglePanKey)}</Styled.KeyCell>
|
||||
<Styled.DescCell>{intl.formatMessage(intlMessages.togglePan)}</Styled.DescCell>
|
||||
</tr>
|
||||
));
|
||||
const shortcutItems = [];
|
||||
shortcutItems.push(renderItem(intl.formatMessage(intlMessages.togglePan), intl.formatMessage(intlMessages.togglePanKey)));
|
||||
shortcutItems.push(renderItem(intl.formatMessage(intlMessages.toggleFullscreen), intl.formatMessage(intlMessages.toggleFullscreenKey)));
|
||||
shortcutItems.push(renderItem(intl.formatMessage(intlMessages.nextSlideDesc), intl.formatMessage(intlMessages.nextSlideKey)));
|
||||
shortcutItems.push(renderItem(intl.formatMessage(intlMessages.previousSlideDesc), intl.formatMessage(intlMessages.previousSlideKey)));
|
||||
|
||||
shortcutItems.push((
|
||||
<tr key={_.uniqueId('hotkey-item-')}>
|
||||
<Styled.KeyCell>{intl.formatMessage(intlMessages.toggleFullscreenKey)}</Styled.KeyCell>
|
||||
<Styled.DescCell>{intl.formatMessage(intlMessages.toggleFullscreen)}</Styled.DescCell>
|
||||
</tr>
|
||||
));
|
||||
|
||||
shortcutItems.push((
|
||||
<tr key={_.uniqueId('hotkey-item-')}>
|
||||
<Styled.KeyCell>{intl.formatMessage(intlMessages.nextSlideKey)}</Styled.KeyCell>
|
||||
<Styled.DescCell>{intl.formatMessage(intlMessages.nextSlideDesc)}</Styled.DescCell>
|
||||
</tr>
|
||||
));
|
||||
|
||||
shortcutItems.push((
|
||||
<tr key={_.uniqueId('hotkey-item-')}>
|
||||
<Styled.KeyCell>{intl.formatMessage(intlMessages.previousSlideKey)}</Styled.KeyCell>
|
||||
<Styled.DescCell>{intl.formatMessage(intlMessages.previousSlideDesc)}</Styled.DescCell>
|
||||
</tr>
|
||||
));
|
||||
const whiteboardShortcutItems = [];
|
||||
//tools
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.select), '1'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.pencil), '2'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.eraser), '3'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.rectangle), '4'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.elipse), '5'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.triangle), '6'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.line), '7'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.arrow), '8'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.text), '9'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.note), '0'));
|
||||
//views
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.zoomIn), 'Ctrl +'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.zoomOut), 'Ctrl -'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.zoomFit), '↑ + 1'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.zoomSelect), '↑ + 2'));
|
||||
//transform
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.flipH), '↑ + H'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.flipV), '↑ + V'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.lock), 'Ctrl ↑ L'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.moveToFront), '↑ ]'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.moveForward), ']'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.moveBackward), '['));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.moveToBack), '↑ ['));
|
||||
//edit
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.undo), 'Ctrl Z'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.redo), 'Ctrl ↑ Z'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.cut), 'Ctrl X'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.copy), 'Ctrl C'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.paste), 'Ctrl V'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.selectAll), 'Ctrl A'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.delete), 'Del'));
|
||||
whiteboardShortcutItems.push(renderItem(intl.formatMessage(intlMessages.duplicate), 'Ctrl D'));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -193,21 +344,69 @@ const ShortcutHelpComponent = (props) => {
|
||||
description: intl.formatMessage(intlMessages.closeDesc),
|
||||
}}
|
||||
>
|
||||
{!accessMod ? <p>{intl.formatMessage(intlMessages.accessKeyNotAvailable)}</p>
|
||||
: (
|
||||
<span>
|
||||
<Styled.ShortcutTable>
|
||||
<Styled.SettingsTabs
|
||||
onSelect={(tab) => setSelectedTab(tab)}
|
||||
selectedIndex={selectedTab}
|
||||
role="presentation"
|
||||
>
|
||||
<StyledSettings.SettingsTabList>
|
||||
<StyledSettings.SettingsTabSelector selectedClassName="is-selected">
|
||||
<StyledSettings.SettingsIcon iconName="application" />
|
||||
<span id="appicationTab">{intl.formatMessage(intlMessages.general)}</span>
|
||||
</StyledSettings.SettingsTabSelector>
|
||||
|
||||
<StyledSettings.SettingsTabSelector selectedClassName="is-selected">
|
||||
<StyledSettings.SettingsIcon iconName="presentation" />
|
||||
<span id="presentationTab">{intl.formatMessage(intlMessages.presentation)}</span>
|
||||
</StyledSettings.SettingsTabSelector>
|
||||
|
||||
<StyledSettings.SettingsTabSelector selectedClassName="is-selected">
|
||||
<StyledSettings.SettingsIcon iconName="whiteboard" />
|
||||
<span id="whiteboardTab">{intl.formatMessage(intlMessages.whiteboard)}</span>
|
||||
</StyledSettings.SettingsTabSelector>
|
||||
</StyledSettings.SettingsTabList>
|
||||
|
||||
<StyledSettings.SettingsTabPanel selectedClassName="is-selected">
|
||||
{!accessMod ? <p>{intl.formatMessage(intlMessages.accessKeyNotAvailable)}</p>
|
||||
: (
|
||||
<span>
|
||||
<Styled.ShortcutTable>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{intl.formatMessage(intlMessages.functionLabel)}</th>
|
||||
<th>{intl.formatMessage(intlMessages.comboLabel)}</th>
|
||||
</tr>
|
||||
{generalShortcutItems}
|
||||
</tbody>
|
||||
</Styled.ShortcutTable>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</StyledSettings.SettingsTabPanel>
|
||||
<StyledSettings.SettingsTabPanel selectedClassName="is-selected">
|
||||
<Styled.ShortcutTable>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{intl.formatMessage(intlMessages.comboLabel)}</th>
|
||||
<th>{intl.formatMessage(intlMessages.functionLabel)}</th>
|
||||
<th>{intl.formatMessage(intlMessages.comboLabel)}</th>
|
||||
</tr>
|
||||
{shortcutItems}
|
||||
</tbody>
|
||||
</Styled.ShortcutTable>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</StyledSettings.SettingsTabPanel>
|
||||
<StyledSettings.SettingsTabPanel selectedClassName="is-selected">
|
||||
<Styled.ShortcutTable>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{intl.formatMessage(intlMessages.functionLabel)}</th>
|
||||
<th>{intl.formatMessage(intlMessages.comboLabel)}</th>
|
||||
</tr>
|
||||
{whiteboardShortcutItems}
|
||||
</tbody>
|
||||
</Styled.ShortcutTable>
|
||||
</StyledSettings.SettingsTabPanel>
|
||||
|
||||
</Styled.SettingsTabs>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@ -1,12 +1,17 @@
|
||||
import styled from 'styled-components';
|
||||
import { borderSize, smPaddingX } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { colorGrayLighter } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { Tabs } from 'react-tabs';
|
||||
|
||||
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
|
||||
const KeyCell = styled.td`
|
||||
border: ${borderSize} solid ${colorGrayLighter};
|
||||
text-align: center;
|
||||
padding: ${smPaddingX};
|
||||
margin: auto;
|
||||
width: 6rem;
|
||||
min-width: 6rem;
|
||||
`;
|
||||
|
||||
const DescCell = styled.td`
|
||||
@ -18,11 +23,25 @@ const DescCell = styled.td`
|
||||
const ShortcutTable = styled.table`
|
||||
border: ${borderSize} solid ${colorGrayLighter};
|
||||
border-collapse: collapse;
|
||||
margin: auto;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const SettingsTabs = styled(Tabs)`
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: flex-start;
|
||||
margin-top: 1rem;
|
||||
|
||||
@media ${smallOnly} {
|
||||
width: 100%;
|
||||
flex-flow: column;
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
KeyCell,
|
||||
DescCell,
|
||||
ShortcutTable,
|
||||
SettingsTabs,
|
||||
};
|
||||
|
@ -134,7 +134,7 @@ const SidebarContent = (props) => {
|
||||
<ErrorBoundary
|
||||
Fallback={FallbackView}
|
||||
>
|
||||
<ChatContainer />
|
||||
<ChatContainer width={width}/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
{sidebarContentPanel === PANELS.SHARED_NOTES && <NotesContainer />}
|
||||
|
@ -180,7 +180,7 @@ const Avatar = styled.div`
|
||||
${({ whiteboardAccess }) => whiteboardAccess && `
|
||||
&:before {
|
||||
content: "\\00a0\\e925\\00a0";
|
||||
padding: ${mdPaddingY};
|
||||
padding: ${mdPaddingY} !important;
|
||||
border-radius: 50% !important;
|
||||
opacity: 1;
|
||||
top: ${userIndicatorsOffset};
|
||||
@ -194,6 +194,7 @@ const Avatar = styled.div`
|
||||
left: auto;
|
||||
right: ${userIndicatorsOffset};
|
||||
letter-spacing: -.33rem;
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
}
|
||||
`}
|
||||
|
@ -9,6 +9,7 @@ import { PANELS, ACTIONS } from '../layout/enums';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import Header from '/imports/ui/components/common/control-header/component';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
waitingUsersTitle: {
|
||||
@ -79,6 +80,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.userList.guest.denyLabel',
|
||||
description: 'Deny guest button label',
|
||||
},
|
||||
feedbackMessage: {
|
||||
id: 'app.userList.guest.feedbackMessage',
|
||||
description: 'Feedback message moderator action',
|
||||
},
|
||||
});
|
||||
|
||||
const ALLOW_STATUS = 'ALLOW';
|
||||
@ -252,11 +257,15 @@ const WaitingUsers = (props) => {
|
||||
setRememberChoice(checked);
|
||||
};
|
||||
|
||||
const changePolicy = (shouldExecutePolicy, policyRule, cb) => () => {
|
||||
const changePolicy = (shouldExecutePolicy, policyRule, cb, message) => () => {
|
||||
if (shouldExecutePolicy) {
|
||||
changeGuestPolicy(policyRule);
|
||||
}
|
||||
|
||||
closePanel();
|
||||
|
||||
notify(intl.formatMessage(intlMessages.feedbackMessage) + message.toUpperCase(), 'success');
|
||||
|
||||
return cb();
|
||||
};
|
||||
|
||||
@ -266,7 +275,7 @@ const WaitingUsers = (props) => {
|
||||
color={color}
|
||||
label={message}
|
||||
size="lg"
|
||||
onClick={changePolicy(rememberChoice, policy, action)}
|
||||
onClick={changePolicy(rememberChoice, policy, action, message)}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -47,6 +47,7 @@ export default function Whiteboard(props) {
|
||||
zoomChanger,
|
||||
isZoomed,
|
||||
isMultiUserActive,
|
||||
isRTL,
|
||||
} = props;
|
||||
|
||||
const { pages, pageStates } = initDefaultPages(curPres?.pages.length || 1);
|
||||
@ -206,7 +207,21 @@ export default function Whiteboard(props) {
|
||||
|
||||
const hasWBAccess = props?.hasMultiUserAccess(props.whiteboardId, props.currentUser.userId);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasWBAccess || isPresenter) {
|
||||
const tdTools = document.getElementById("TD-Tools");
|
||||
if (tdTools) {
|
||||
// removes tldraw native help menu button
|
||||
tdTools.parentElement?.nextSibling?.remove();
|
||||
}
|
||||
// removes image tool from the tldraw toolbar
|
||||
document.getElementById("TD-PrimaryTools-Image").style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
const onMount = (app) => {
|
||||
app.setSetting('language', document.getElementsByTagName('html')[0]?.lang || 'en');
|
||||
app.setSetting('dockPosition', isRTL ? 'left' : 'right');
|
||||
setTLDrawAPI(app);
|
||||
props.setTldrawAPI(app);
|
||||
// disable for non presenter that doesn't have multi user access
|
||||
@ -247,7 +262,7 @@ export default function Whiteboard(props) {
|
||||
}
|
||||
};
|
||||
|
||||
const onPatch = (s, reason) => {
|
||||
const onPatch = (e, t, reason) => {
|
||||
if (reason && isPresenter && (reason.includes("zoomed") || reason.includes("panned"))) {
|
||||
if (cameraFitSlide.zoom === 0) {
|
||||
//can happen when the slide finish uploading
|
||||
@ -344,8 +359,7 @@ export default function Whiteboard(props) {
|
||||
}
|
||||
}}
|
||||
onCommand={(e, s, g) => {
|
||||
|
||||
if (s.includes('move_to_page')) {
|
||||
if (s?.id.includes('move_to_page')) {
|
||||
let groupShapes = [];
|
||||
let nonGroupShapes = [];
|
||||
let movedShapes = {};
|
||||
@ -373,7 +387,7 @@ export default function Whiteboard(props) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s.includes('ungroup')) {
|
||||
if (s?.id.includes('ungroup')) {
|
||||
e?.selectedIds?.map(id => {
|
||||
persistShape(e.getShape(id), whiteboardId);
|
||||
})
|
||||
@ -390,9 +404,10 @@ export default function Whiteboard(props) {
|
||||
|
||||
const conditions = [
|
||||
"session:complete", "style", "updated_shapes", "duplicate", "stretch",
|
||||
"align", "move", "delete", "create", "flip", "toggle", "group", "translate"
|
||||
"align", "move", "delete", "create", "flip", "toggle", "group", "translate",
|
||||
"transform_single", "arrow", "edit", "erase", "rotate",
|
||||
]
|
||||
if (conditions.some(el => s?.startsWith(el))) {
|
||||
if (conditions.some(el => s?.id?.startsWith(el))) {
|
||||
e.selectedIds.forEach(id => {
|
||||
const shape = e.getShape(id);
|
||||
const shapeBounds = e.getShapeBounds(id);
|
||||
|
@ -5,13 +5,15 @@ import React, { useContext } from "react";
|
||||
import { UsersContext } from "../components-data/users-context/context";
|
||||
import Auth from "/imports/ui/services/auth";
|
||||
import PresentationToolbarService from '../presentation/presentation-toolbar/service';
|
||||
import { layoutSelect } from '../layout/context';
|
||||
|
||||
const WhiteboardContainer = (props) => {
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
const isRTL = layoutSelect((i) => i.isRTL);
|
||||
const { users } = usingUsersContext;
|
||||
const currentUser = users[Auth.meetingID][Auth.userID];
|
||||
const isPresenter = currentUser.presenter;
|
||||
return <Whiteboard {...{isPresenter, currentUser}} {...props} meetingId={Auth.meetingID} />
|
||||
return <Whiteboard {...{isPresenter, currentUser, isRTL}} {...props} meetingId={Auth.meetingID} />
|
||||
};
|
||||
|
||||
export default withTracker(({ whiteboardId, curPageId, intl, zoomChanger }) => {
|
||||
|
@ -55,3 +55,11 @@ export function isVirtualBackgroundsEnabled() {
|
||||
export function isCustomVirtualBackgroundsEnabled() {
|
||||
return getDisabledFeatures().indexOf('customVirtualBackgrounds') === -1;
|
||||
}
|
||||
|
||||
export function isDownloadPresentationWithAnnotationsEnabled() {
|
||||
return getDisabledFeatures().indexOf('downloadPresentationWithAnnotations') === -1 && Meteor.settings.public.presentation.allowDownloadable;
|
||||
}
|
||||
|
||||
export function isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled() {
|
||||
return getDisabledFeatures().indexOf('importPresentationWithAnnotationsFromBreakoutRooms') === -1;
|
||||
}
|
||||
|
@ -21,7 +21,12 @@ export const unregisterTitleView = () => {
|
||||
title.text = data.join(' - ');
|
||||
};
|
||||
|
||||
export const convertRemToPixels = (rem) => {
|
||||
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
}
|
||||
|
||||
export default {
|
||||
registerTitleView,
|
||||
unregisterTitleView,
|
||||
convertRemToPixels,
|
||||
};
|
||||
|
327
bigbluebutton-html5/package-lock.json
generated
327
bigbluebutton-html5/package-lock.json
generated
@ -712,23 +712,6 @@
|
||||
"@radix-ui/react-primitive": "0.1.4"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-checkbox": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-0.1.5.tgz",
|
||||
"integrity": "sha512-M8Y4dSXsKSbF+FryG5VvZKr/1MukMVG7swq9p5s7wYb8Rvn0UM0rQ5w8BWmSWSV4BL/gbJdhwVCznwXXlgZRZg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "0.1.0",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-label": "0.1.5",
|
||||
"@radix-ui/react-presence": "0.1.2",
|
||||
"@radix-ui/react-primitive": "0.1.4",
|
||||
"@radix-ui/react-use-controllable-state": "0.1.0",
|
||||
"@radix-ui/react-use-previous": "0.1.1",
|
||||
"@radix-ui/react-use-size": "0.1.1"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-collection": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-0.1.4.tgz",
|
||||
@ -854,18 +837,6 @@
|
||||
"@radix-ui/react-use-layout-effect": "0.1.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-label": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-0.1.5.tgz",
|
||||
"integrity": "sha512-Au9+n4/DhvjR0IHhvZ1LPdx/OW+3CGDie30ZyCkbSHIuLp4/CV4oPPGBwJ1vY99Jog3zyQhsGww9MXj8O9Aj/A==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-id": "0.1.5",
|
||||
"@radix-ui/react-primitive": "0.1.4"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-menu": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-0.1.6.tgz",
|
||||
@ -891,6 +862,28 @@
|
||||
"react-remove-scroll": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-popover": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-0.1.6.tgz",
|
||||
"integrity": "sha512-zQzgUqW4RQDb0ItAL1xNW4K4olUrkfV3jeEPs9rG+nsDQurO+W9TT+YZ9H1mmgAJqlthyv1sBRZGdBm4YjtD6Q==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "0.1.0",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "0.1.5",
|
||||
"@radix-ui/react-focus-guards": "0.1.0",
|
||||
"@radix-ui/react-focus-scope": "0.1.4",
|
||||
"@radix-ui/react-id": "0.1.5",
|
||||
"@radix-ui/react-popper": "0.1.4",
|
||||
"@radix-ui/react-portal": "0.1.4",
|
||||
"@radix-ui/react-presence": "0.1.2",
|
||||
"@radix-ui/react-primitive": "0.1.4",
|
||||
"@radix-ui/react-use-controllable-state": "0.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-popper": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-0.1.4.tgz",
|
||||
@ -936,24 +929,6 @@
|
||||
"@radix-ui/react-slot": "0.1.2"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-radio-group": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-0.1.5.tgz",
|
||||
"integrity": "sha512-ybgHsmh/V2crKvK6xZ56dpPul7b+vyxcq7obWqHbr5W6Ca11wdm0E7lS0i/Y6pgfIKYOWIARmZYDpRMEeRCPOw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "0.1.0",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-label": "0.1.5",
|
||||
"@radix-ui/react-presence": "0.1.2",
|
||||
"@radix-ui/react-primitive": "0.1.4",
|
||||
"@radix-ui/react-roving-focus": "0.1.5",
|
||||
"@radix-ui/react-use-controllable-state": "0.1.0",
|
||||
"@radix-ui/react-use-previous": "0.1.1",
|
||||
"@radix-ui/react-use-size": "0.1.1"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-roving-focus": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-0.1.5.tgz",
|
||||
@ -1100,14 +1075,15 @@
|
||||
"integrity": "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA=="
|
||||
},
|
||||
"@tldraw/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tldraw/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-52tRphl8sOQj5oglbhDe4KSetfuQ1ZRpMewkRU6l3XV3OA+ipLk/y6MHaEm8eymoDeWeox/QaiV+URqK/89+gQ==",
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@tldraw/core/-/core-1.15.0.tgz",
|
||||
"integrity": "sha512-4A9Xyh/VQBCtzVxZF/a7pXkP6AbFqVE8LcD9gmKWnaSYW6haLhc/Ie1O477T5xKV2HMfWJ52x1dUEn3jOGFVCg==",
|
||||
"requires": {
|
||||
"@tldraw/intersect": "^1.7.1",
|
||||
"@tldraw/vec": "^1.7.0",
|
||||
"@use-gesture/react": "^10.2.4",
|
||||
"@tldraw/vec": "^1.7.1",
|
||||
"@use-gesture/react": "^10.2.14",
|
||||
"mobx-react-lite": "^3.2.3",
|
||||
"perfect-freehand": "^1.1.0",
|
||||
"resize-observer-polyfill": "^1.5.1"
|
||||
}
|
||||
},
|
||||
@ -1120,33 +1096,148 @@
|
||||
}
|
||||
},
|
||||
"@tldraw/tldraw": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@tldraw/tldraw/-/tldraw-1.8.1.tgz",
|
||||
"integrity": "sha512-JpMS3+JfyJQpPw55DAwbxL8YCvVx9xSseL1Bx+72LhAydl+Gpt42Cc8Bw+DiTXg/eVwE0/rfy7ZvClNgLcnNWQ==",
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@tldraw/tldraw/-/tldraw-1.20.0.tgz",
|
||||
"integrity": "sha512-dVy/l7ceTRwEBzpFvmT2vqDCmbiAJtF55Jv6At9Y9oNDF+xT81dgXupHVw0wSiQRW3ZwD2hxPjnFWqi6uHBS+w==",
|
||||
"requires": {
|
||||
"@radix-ui/react-alert-dialog": "^0.1.5",
|
||||
"@radix-ui/react-checkbox": "^0.1.4",
|
||||
"@radix-ui/react-context-menu": "^0.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^0.1.4",
|
||||
"@radix-ui/react-icons": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^0.1.4",
|
||||
"@radix-ui/react-tooltip": "^0.1.6",
|
||||
"@stitches/react": "^1.2.6",
|
||||
"@tldraw/core": "^1.8.0",
|
||||
"@radix-ui/react-alert-dialog": "^0.1.7",
|
||||
"@radix-ui/react-context-menu": "^0.1.6",
|
||||
"@radix-ui/react-dialog": "^0.1.7",
|
||||
"@radix-ui/react-dropdown-menu": "^0.1.6",
|
||||
"@radix-ui/react-icons": "^1.1.1",
|
||||
"@radix-ui/react-popover": "^0.1.6",
|
||||
"@radix-ui/react-tooltip": "^0.1.7",
|
||||
"@stitches/react": "^1.2.8",
|
||||
"@tldraw/core": "^1.15.0",
|
||||
"@tldraw/intersect": "^1.7.1",
|
||||
"@tldraw/vec": "^1.7.0",
|
||||
"@tldraw/vec": "^1.7.1",
|
||||
"idb-keyval": "^6.1.0",
|
||||
"perfect-freehand": "^1.0.16",
|
||||
"react-hotkey-hook": "^1.0.2",
|
||||
"lz-string": "^1.4.4",
|
||||
"perfect-freehand": "^1.1.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hotkeys-hook": "^3.4.4",
|
||||
"tslib": "^2.3.1",
|
||||
"react-intl": "^6.0.3",
|
||||
"tslib": "^2.4.0",
|
||||
"zustand": "^3.6.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": {
|
||||
"version": "1.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.8.tgz",
|
||||
"integrity": "sha512-fgLqyWlwmTEuqV/TSLEL/t9JOmHNLFvCdgzXB0jc2w+WOItPCOJ1T0eyN6fQBQKRPfSqqNlu+kWj7ijcOVTVVQ==",
|
||||
"requires": {
|
||||
"@formatjs/intl-localematcher": "0.2.28",
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/fast-memoize": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.4.tgz",
|
||||
"integrity": "sha512-9ARYoLR8AEzXvj2nYrOVHY/h1dDMDWGTnKDLXSISF1uoPakSmfcZuSqjiqZX2wRkEUimPxdwTu/agyozBtZRHA==",
|
||||
"requires": {
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/icu-messageformat-parser": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.4.tgz",
|
||||
"integrity": "sha512-3PqMvKWV1oyok0BuiXUAHIaotdhdTJw6OICqCZbfUgKT+ZRwRWO4IlCgvXJeCITaKS5p+PY0XXKjf/vUyIpWjQ==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.8",
|
||||
"@formatjs/icu-skeleton-parser": "1.3.10",
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/icu-skeleton-parser": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.10.tgz",
|
||||
"integrity": "sha512-kXJmtLDqFF5aLTf8IxdJXnhrIX1Qb4Qp3a9jqRecGDYfzOa9hMhi9U0nKyhrJJ4cXxBzptcgb+LWkyeHL6nlBQ==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.8",
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/intl": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.3.1.tgz",
|
||||
"integrity": "sha512-f06qZ/ukpeN24gc01qFjh3P+r3FU/ikY4yG+fDJu6dPNvpUQzDy98lYogA1dr6ig2UtrnoEk3xncyFPL1e9cZw==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.8",
|
||||
"@formatjs/fast-memoize": "1.2.4",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.4",
|
||||
"@formatjs/intl-displaynames": "6.0.3",
|
||||
"@formatjs/intl-listformat": "7.0.3",
|
||||
"intl-messageformat": "10.1.1",
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/intl-displaynames": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.0.3.tgz",
|
||||
"integrity": "sha512-Mxh6W1VOlmiEvO/QPBrBQHlXrIn5VxjJWyyEI0V7ZHNGl0ee8AjSlq7vIJG8GodRJqGUuutF6N3OB/6qFv0YWg==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.8",
|
||||
"@formatjs/intl-localematcher": "0.2.28",
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/intl-listformat": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.0.3.tgz",
|
||||
"integrity": "sha512-ampNLRGZl/08epHa3i5sRmcHGLneC6JrknexbbgnexYFNSmJ6AbL/dCzgrQzw2Efl+5AZK7UbNFxcDYY3RePvw==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.8",
|
||||
"@formatjs/intl-localematcher": "0.2.28",
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/intl-localematcher": {
|
||||
"version": "0.2.28",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.28.tgz",
|
||||
"integrity": "sha512-FLsc6Gifs1np/8HnCn/7Q+lHMmenrD5fuDhRT82yj0gi9O19kfaFwjQUw1gZsyILuRyT93GuzdifHj7TKRhBcw==",
|
||||
"requires": {
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"intl-messageformat": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.1.1.tgz",
|
||||
"integrity": "sha512-FeJne2oooYW6shLPbrqyjRX6hTELVrQ90Dn88z7NomLk/xZBCLxLPAkgaYaTQJBRBV78nZ933d8APHHkTQrD9Q==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.8",
|
||||
"@formatjs/fast-memoize": "1.2.4",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.4",
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"react-intl": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.0.5.tgz",
|
||||
"integrity": "sha512-nDZ3BosuE8WdovcGxsrjj1aIgJZklSL5aORs5oah+5tLQTzUdOEstzJEYQPM+sxl1dkDOu7RCuw0z9oI9ENf9g==",
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.11.8",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.4",
|
||||
"@formatjs/intl": "2.3.1",
|
||||
"@formatjs/intl-displaynames": "6.0.3",
|
||||
"@formatjs/intl-listformat": "7.0.3",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"@types/react": "16 || 17 || 18",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"intl-messageformat": "10.1.1",
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tldraw/vec": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@tldraw/vec/-/vec-1.7.0.tgz",
|
||||
"integrity": "sha512-uidcNCtm6kL6M4GcXvPb0+WxVeJ3H1csYqsPwDNhVwIrF6eCUKNCoh+70G0kUk2S0EIMmufp3COhagIS8Xnqig=="
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@tldraw/vec/-/vec-1.7.1.tgz",
|
||||
"integrity": "sha512-qM6Z9RvkLFFEzr91mmsA4HI14msyDgDDOu36csIzG5BYu2bFmEz5siQ8WntHgDtUjzJHP+VSSOTbAXhklEZHLA=="
|
||||
},
|
||||
"@types/hoist-non-react-statics": {
|
||||
"version": "3.3.1",
|
||||
@ -1198,16 +1289,16 @@
|
||||
"integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA=="
|
||||
},
|
||||
"@use-gesture/core": {
|
||||
"version": "10.2.13",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.2.13.tgz",
|
||||
"integrity": "sha512-o+IDJm94ztxsNlsYYT9SF74swJm6WMqmu6oV/LQzbw2E9iIOUW0CanJmrO1ksEOmSMr7W+xVdAge8FJhT/MGEQ=="
|
||||
"version": "10.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.2.17.tgz",
|
||||
"integrity": "sha512-62hCybe4x6oGZ1/JA9gSYIdghV1FqxCdvYWt9SqCEAAikwT1OmVl2Q/Uu8CP636L57D+DfXtw6PWM+fdhr4oJQ=="
|
||||
},
|
||||
"@use-gesture/react": {
|
||||
"version": "10.2.13",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.2.13.tgz",
|
||||
"integrity": "sha512-8A4EfyEtJ09njHl28DH7h8fYkbWGXCPGoP/RXZ8eQQHKIDT4GwnagWDM+KfwkuBu+v98MG+IOq9RjzZkSfcwNg==",
|
||||
"version": "10.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.2.17.tgz",
|
||||
"integrity": "sha512-Vfrp1KgdYn/kOEUAYNXtGBCl2dr38s3G6rru1TOPs+cVUjfNyNxvJK56grUyJ336N3rQLK8F9G7+FfrHuc3g/Q==",
|
||||
"requires": {
|
||||
"@use-gesture/core": "10.2.13"
|
||||
"@use-gesture/core": "10.2.17"
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
@ -3332,9 +3423,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"hotkeys-js": {
|
||||
"version": "3.9.3",
|
||||
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.9.3.tgz",
|
||||
"integrity": "sha512-s+f0xyvDmf6+DyrFQ2SY+eA7lbvMbjqkqi0I0SpMgnN5tZx7DeH8nsWhkJR4KEq3pxDPHJppDUhdt1rZFW5LeQ=="
|
||||
"version": "3.9.4",
|
||||
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.9.4.tgz",
|
||||
"integrity": "sha512-2zuLt85Ta+gIyvs4N88pCYskNrxf1TFv3LR9t5mdAZIX8BcgQQ48F2opUptvHa6m8zsy5v/a0i9mWzTrlNWU0Q=="
|
||||
},
|
||||
"html-to-image": {
|
||||
"version": "1.9.0",
|
||||
@ -3451,9 +3542,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"idb-keyval": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.1.0.tgz",
|
||||
"integrity": "sha512-u/qHZ75rlD3gH+Zah8dAJVJcGW/RfCnfNrFkElC5RpRCnpsCXXhqjVk+6MoVKJ3WhmNbRYdI6IIVP88e+5sxGw==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.0.tgz",
|
||||
"integrity": "sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==",
|
||||
"requires": {
|
||||
"safari-14-idb-fix": "^3.0.0"
|
||||
}
|
||||
@ -4175,6 +4266,11 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"lz-string": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
||||
"integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ=="
|
||||
},
|
||||
"makeup-screenreader-trap": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/makeup-screenreader-trap/-/makeup-screenreader-trap-0.0.5.tgz",
|
||||
@ -5540,9 +5636,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"perfect-freehand": {
|
||||
"version": "1.0.16",
|
||||
"resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.0.16.tgz",
|
||||
"integrity": "sha512-D4+avUeR8CHSl2vaPbPYX/dNpSMRYO3VOFp7qSSc+LRkSgzQbLATVnXosy7VxtsSHEh1C5t8K8sfmo0zCVnfWQ=="
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.1.0.tgz",
|
||||
"integrity": "sha512-nVWukMN9qlii1dQsQHVvfaNpeOAWVLgTZP6e/tFcU6cWlLo+6YdvfRGBL2u5pU11APlPbHeB0SpMcGA8ZjPgcQ=="
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
@ -5893,12 +5989,12 @@
|
||||
}
|
||||
},
|
||||
"react-draggable": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-3.3.2.tgz",
|
||||
"integrity": "sha512-oaz8a6enjbPtx5qb0oDWxtDNuybOylvto1QLydsXgKmwT7e3GXC2eMVDwEMIUYJIFqVG72XpOv673UuuAq6LhA==",
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.5.tgz",
|
||||
"integrity": "sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==",
|
||||
"requires": {
|
||||
"classnames": "^2.2.5",
|
||||
"prop-types": "^15.6.0"
|
||||
"clsx": "^1.1.1",
|
||||
"prop-types": "^15.8.1"
|
||||
}
|
||||
},
|
||||
"react-dropzone": {
|
||||
@ -5910,22 +6006,25 @@
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"react-error-boundary": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
|
||||
"integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
}
|
||||
},
|
||||
"react-fast-compare": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
|
||||
},
|
||||
"react-hotkey-hook": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-hotkey-hook/-/react-hotkey-hook-1.0.2.tgz",
|
||||
"integrity": "sha512-95GiOW8ORMqbBQ23+VHMF0giRmpiI8sFHPjbOR/e64zWI0QT+QO3Q/022c0HNBS/LrQsbGdjm64BNMah0WvlnA=="
|
||||
},
|
||||
"react-hotkeys-hook": {
|
||||
"version": "3.4.6",
|
||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.6.tgz",
|
||||
"integrity": "sha512-SiGKHnauaAQglRA7qeiW5LTa0KoT2ssv8YGYKZQoM3P9v5JFEHJdXOSFml1N6K86oKQ8dLCLlxqBqGlSJWGmxQ==",
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.7.tgz",
|
||||
"integrity": "sha512-+bbPmhPAl6ns9VkXkNNyxlmCAIyDAcWbB76O4I0ntr3uWCRuIQf/aRLartUahe9chVMPj+OEzzfk3CQSjclUEQ==",
|
||||
"requires": {
|
||||
"hotkeys-js": "3.9.3"
|
||||
"hotkeys-js": "3.9.4"
|
||||
}
|
||||
},
|
||||
"react-intl": {
|
||||
@ -5984,30 +6083,30 @@
|
||||
}
|
||||
},
|
||||
"react-remove-scroll": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.3.tgz",
|
||||
"integrity": "sha512-NQ1bXrxKrnK5pFo/GhLkXeo3CrK5steI+5L+jynwwIemvZyfXqaL0L5BzwJd7CSwNCU723DZaccvjuyOdoy3Xw==",
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
|
||||
"integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
|
||||
"requires": {
|
||||
"react-remove-scroll-bar": "^2.3.1",
|
||||
"react-style-singleton": "^2.2.0",
|
||||
"tslib": "^2.0.0",
|
||||
"react-remove-scroll-bar": "^2.3.3",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"react-remove-scroll-bar": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.1.tgz",
|
||||
"integrity": "sha512-IvGX3mJclEF7+hga8APZczve1UyGMkMG+tjS0o/U1iLgvZRpjFAQEUBJ4JETfvbNlfNnZnoDyWJCICkA15Mghg==",
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.3.tgz",
|
||||
"integrity": "sha512-i9GMNWwpz8XpUpQ6QlevUtFjHGqnPG4Hxs+wlIJntu/xcsZVEpJcIV71K3ZkqNy2q3GfgvkD7y6t/Sv8ofYSbw==",
|
||||
"requires": {
|
||||
"react-style-singleton": "^2.2.0",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"react-style-singleton": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.0.tgz",
|
||||
"integrity": "sha512-nK7mN92DMYZEu3cQcAhfwE48NpzO5RpxjG4okbSqRRbfal9Pk+fG2RdQXTMp+f6all1hB9LIJSt+j7dCYrU11g==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
||||
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
|
||||
"requires": {
|
||||
"get-nonce": "^1.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
|
@ -32,9 +32,8 @@
|
||||
"@browser-bunyan/server-stream": "^1.8.0",
|
||||
"@jitsi/sdp-interop": "0.1.14",
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@mconf/bbb-diff": "1.2.0",
|
||||
"@tldraw/core": "^1.9.1",
|
||||
"@tldraw/tldraw": "1.8.1",
|
||||
"@tldraw/core": "1.15.0",
|
||||
"@tldraw/tldraw": "1.20.0",
|
||||
"@mconf/bbb-diff": "^1.2.0",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"axios": "^0.21.3",
|
||||
@ -66,7 +65,7 @@
|
||||
"react-autosize-textarea": "^5.0.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-draggable": "^3.3.2",
|
||||
"react-draggable": "^4.4.5",
|
||||
"react-dropzone": "^7.0.1",
|
||||
"react-intl": "^5.25.1",
|
||||
"react-loading-skeleton": "^3.0.3",
|
||||
|
@ -507,6 +507,8 @@ public:
|
||||
moderatorChatEmphasized: true
|
||||
autoConvertEmoji: true
|
||||
enableEmojiPicker: false
|
||||
# e.g.: disableEmojis: ['1F595','1F922']
|
||||
disableEmojis: []
|
||||
notes:
|
||||
enabled: true
|
||||
id: notes
|
||||
|
@ -705,6 +705,7 @@
|
||||
"app.userList.guest.privateMessageLabel": "Message",
|
||||
"app.userList.guest.acceptLabel": "Accept",
|
||||
"app.userList.guest.denyLabel": "Deny",
|
||||
"app.userList.guest.feedbackMessage": "Action applied: ",
|
||||
"app.user-info.title": "Directory Lookup",
|
||||
"app.toast.breakoutRoomEnded": "The breakout room ended. Please rejoin in the audio.",
|
||||
"app.toast.chat.public": "New Public Chat message",
|
||||
@ -750,6 +751,38 @@
|
||||
"app.shortcut-help.toggleFullscreenKey": "Enter",
|
||||
"app.shortcut-help.nextSlideKey": "Right Arrow",
|
||||
"app.shortcut-help.previousSlideKey": "Left Arrow",
|
||||
"app.shortcut-help.select": "Select Tool",
|
||||
"app.shortcut-help.pencil": "Pencil",
|
||||
"app.shortcut-help.eraser": "Eraser",
|
||||
"app.shortcut-help.rectangle": "Rectangle",
|
||||
"app.shortcut-help.elipse": "Elipse",
|
||||
"app.shortcut-help.triangle": "Triangle",
|
||||
"app.shortcut-help.line": "Line",
|
||||
"app.shortcut-help.arrow": "Arrow",
|
||||
"app.shortcut-help.text": "Text Tool",
|
||||
"app.shortcut-help.note": "Sticky Note",
|
||||
"app.shortcut-help.general": "General",
|
||||
"app.shortcut-help.presentation": "Presentation",
|
||||
"app.shortcut-help.whiteboard": "Whiteboard",
|
||||
"app.shortcut-help.zoomIn": "Zoom In",
|
||||
"app.shortcut-help.zoomOut": "Zoom Out",
|
||||
"app.shortcut-help.zoomFit": "Zoom to Fit",
|
||||
"app.shortcut-help.zoomSelect": "Zoom to Selection",
|
||||
"app.shortcut-help.flipH": "Flip Horozontal",
|
||||
"app.shortcut-help.flipV": "Flip Vertical",
|
||||
"app.shortcut-help.lock": "Lock / Unlock",
|
||||
"app.shortcut-help.moveToFront": "Move to Front",
|
||||
"app.shortcut-help.moveToBack": "Move to Back",
|
||||
"app.shortcut-help.moveForward": "Move Forward",
|
||||
"app.shortcut-help.moveBackward": "Move Backward",
|
||||
"app.shortcut-help.undo": "Undo",
|
||||
"app.shortcut-help.redo": "Redo",
|
||||
"app.shortcut-help.cut": "Cut",
|
||||
"app.shortcut-help.copy": "Copy",
|
||||
"app.shortcut-help.paste": "Paste",
|
||||
"app.shortcut-help.selectAll": "Select All",
|
||||
"app.shortcut-help.delete": "Delete",
|
||||
"app.shortcut-help.duplicate": "Duplicate",
|
||||
"app.lock-viewers.title": "Lock viewers",
|
||||
"app.lock-viewers.description": "These options enable you to restrict viewers from using specific features.",
|
||||
"app.lock-viewers.featuresLable": "Feature",
|
||||
@ -1113,6 +1146,11 @@
|
||||
"app.learningDashboard.userDetails.response": "Response",
|
||||
"app.learningDashboard.userDetails.mostCommonAnswer": "Most Common Answer",
|
||||
"app.learningDashboard.userDetails.anonymousAnswer": "Anonymous Poll",
|
||||
"app.learningDashboard.userDetails.talkTime": "Talk Time",
|
||||
"app.learningDashboard.userDetails.messages": "Messages",
|
||||
"app.learningDashboard.userDetails.emojis": "Emojis",
|
||||
"app.learningDashboard.userDetails.raiseHands": "Raise Hands",
|
||||
"app.learningDashboard.userDetails.pollVotes": "Poll Votes",
|
||||
"app.learningDashboard.usersTable.title": "Overview",
|
||||
"app.learningDashboard.usersTable.colOnline": "Online time",
|
||||
"app.learningDashboard.usersTable.colTalk": "Talk time",
|
||||
|
@ -3,14 +3,14 @@ const { MultiUsers } = require('../user/multiusers');
|
||||
const { Audio } = require('./audio');
|
||||
|
||||
test.describe.parallel('Audio', () => {
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#listen-only-mode-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#listen-only-mode-automated
|
||||
test('Join audio with Listen Only @ci', async ({ browser, page }) => {
|
||||
const audio = new Audio(browser, page);
|
||||
await audio.init(true, false);
|
||||
await audio.joinAudio();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#join-audio-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#join-audio-automated
|
||||
test('Join audio with Microphone @ci', async ({ browser, page }) => {
|
||||
const audio = new Audio(browser, page);
|
||||
await audio.init(true, false);
|
||||
|
@ -9,7 +9,7 @@ test.describe.parallel('Breakout', () => {
|
||||
await create.create();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#moderators-creating-breakout-rooms-and-assiging-users-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#moderators-creating-breakout-rooms-and-assiging-users-automated
|
||||
test('Join Breakout room @ci', async ({ browser, context, page }) => {
|
||||
const join = new Join(browser, context);
|
||||
await join.initPages(page);
|
||||
|
@ -3,14 +3,14 @@ const { Chat } = require('./chat');
|
||||
const { PrivateChat } = require('./privateChat');
|
||||
|
||||
test.describe.parallel('Chat', () => {
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#public-message-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#public-message-automated
|
||||
test('Send public message @ci', async ({ browser, page }) => {
|
||||
const chat = new Chat(browser, page);
|
||||
await chat.init(true, true);
|
||||
await chat.sendPublicMessage();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#private-message-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#private-message-automated
|
||||
test('Send private message @ci', async ({ browser, context, page }) => {
|
||||
const privateChat = new PrivateChat(browser, context);
|
||||
await privateChat.initPages(page);
|
||||
@ -36,14 +36,14 @@ test.describe.parallel('Chat', () => {
|
||||
await chat.saveChat(testInfo);
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#chat-character-limit-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#chat-character-limit-automated
|
||||
test('Verify character limit', async ({ browser, page }) => {
|
||||
const chat = new Chat(browser, page);
|
||||
await chat.init(true, true);
|
||||
await chat.characterLimit();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#sending-empty-chat-message-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#sending-empty-chat-message-automated
|
||||
test('Not able to send an empty message', async ({ browser, page }) => {
|
||||
const chat = new Chat(browser, page);
|
||||
await chat.init(true, true);
|
||||
|
@ -3,7 +3,6 @@ const { MultiUsers } = require('../user/multiusers');
|
||||
const e = require('../core/elements');
|
||||
const { ELEMENT_WAIT_TIME } = require('../core/constants');
|
||||
const { openConnectionStatus, checkNetworkStatus } = require('./util');
|
||||
const { sleep } = require('../core/helpers');
|
||||
|
||||
class ConnectionStatus extends MultiUsers {
|
||||
constructor(browser, context) {
|
||||
|
@ -299,3 +299,8 @@ exports.multiUsersWhiteboardOff = 'button[data-test="turnMultiUsersWhiteboardOff
|
||||
exports.whiteboardViewBox = 'svg g[clip-path="url(#viewBox)"]';
|
||||
exports.changeWhiteboardAccess = 'li[data-test="changeWhiteboardAccess"]';
|
||||
exports.pencil = 'button[data-test="pencilTool"]';
|
||||
|
||||
// Shared notes
|
||||
exports.showMoreSharedNotesButton = 'span[class="show-more-icon-btn"]'
|
||||
exports.exportSharedNotesButton = 'button[aria-label="Import/Export from/to different file formats"]';
|
||||
exports.exportPlainButton = 'span[id="exportplain"]';
|
||||
|
@ -156,11 +156,21 @@ class Page {
|
||||
await expect(locator).toBeHidden({ timeout });
|
||||
}
|
||||
|
||||
async wasNthElementRemoved(selector, count, timeout = ELEMENT_WAIT_TIME) {
|
||||
const locator = this.getLocator(':nth-match(' + selector + ',' + count + ')');
|
||||
await expect(locator).toBeHidden({ timeout });
|
||||
}
|
||||
|
||||
async hasElement(selector, timeout = ELEMENT_WAIT_TIME) {
|
||||
const locator = this.getLocator(selector);
|
||||
await expect(locator).toBeVisible({ timeout });
|
||||
}
|
||||
|
||||
async hasNElements(selector, count, timeout = ELEMENT_WAIT_TIME) {
|
||||
const locator = this.getLocator(':nth-match(' + selector + ',' + count + ')');
|
||||
await expect(locator).toBeVisible({ timeout });
|
||||
}
|
||||
|
||||
async hasElementDisabled(selector, timeout = ELEMENT_WAIT_TIME) {
|
||||
const locator = this.getLocator(selector);
|
||||
await expect(locator).toBeDisabled({ timeout });
|
||||
@ -175,6 +185,18 @@ class Page {
|
||||
const locator = this.getLocator(selector);
|
||||
await expect(locator).toContainText(text, { timeout });
|
||||
}
|
||||
|
||||
async press(key) {
|
||||
await this.page.keyboard.press(key);
|
||||
}
|
||||
|
||||
async down(key) {
|
||||
await this.page.keyboard.down(key);
|
||||
}
|
||||
|
||||
async up(key) {
|
||||
await this.page.keyboard.up(key);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = Page;
|
||||
|
@ -9,14 +9,14 @@ test.describe.parallel('Polling', () => {
|
||||
await polling.createPoll();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#start-an-anonymous-poll-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#start-an-anonymous-poll-automated
|
||||
test('Create anonymous poll @ci', async ({ browser, context, page }) => {
|
||||
const polling = new Polling(browser, context);
|
||||
await polling.initPages(page);
|
||||
await polling.pollAnonymous();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#quick-poll-option-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#quick-poll-option-automated
|
||||
test('Create quick poll - from the slide', async ({ browser, context, page }) => {
|
||||
const polling = new Polling(browser, context);
|
||||
await polling.initPages(page);
|
||||
|
@ -2,24 +2,28 @@ const { test } = require('@playwright/test');
|
||||
const { Presentation } = require('./presentation');
|
||||
|
||||
test.describe.parallel('Presentation', () => {
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#navigation-automated
|
||||
test('Skip slide @ci', async ({ browser, context, page }) => {
|
||||
const presentation = new Presentation(browser, context);
|
||||
await presentation.initPages(page);
|
||||
await presentation.skipSlide();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#minimizerestore-presentation-automated
|
||||
test('Hide/Restore presentation @ci', async ({ browser, context, page }) => {
|
||||
const presentation = new Presentation(browser, context);
|
||||
await presentation.initPages(page);
|
||||
await presentation.hideAndRestorePresentation();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#start-youtube-video-sharing
|
||||
test('Start external video', async ({ browser, context, page }) => {
|
||||
const presentation = new Presentation(browser, context);
|
||||
await presentation.initPages(page);
|
||||
await presentation.startExternalVideo();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#fit-to-width-option
|
||||
test('Presentation fit to width', async ({ browser, context, page }) => {
|
||||
const presentation = new Presentation(browser, context);
|
||||
await presentation.initPages(page);
|
||||
@ -27,7 +31,7 @@ test.describe.parallel('Presentation', () => {
|
||||
});
|
||||
|
||||
test.describe.parallel('Manage', () => {
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#uploading-a-presentation-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#uploading-a-presentation-automated
|
||||
test('Upload single presentation @ci', async ({ browser, context, page }) => {
|
||||
test.fixme(true, 'Different behaviors in the development and production environment');
|
||||
const presentation = new Presentation(browser, context);
|
||||
@ -35,13 +39,14 @@ test.describe.parallel('Presentation', () => {
|
||||
await presentation.uploadSinglePresentationTest();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#uploading-multiple-presentations-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#uploading-multiple-presentations-automated
|
||||
test('Upload multiple presentations', async ({ browser, context, page }) => {
|
||||
const presentation = new Presentation(browser, context);
|
||||
await presentation.initPages(page, true);
|
||||
await presentation.uploadMultiplePresentationsTest();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#enabling-and-disabling-presentation-download-automated
|
||||
test.skip('Allow and disallow presentation download @ci', async ({ browser, context, page }, testInfo) => {
|
||||
const presentation = new Presentation(browser, context);
|
||||
await presentation.initPages(page);
|
||||
|
@ -2,6 +2,7 @@ const { test, devices } = require('@playwright/test');
|
||||
const { ScreenShare } = require('./screenshare');
|
||||
|
||||
test.describe.parallel('Screenshare', () => {
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#sharing-screen-in-full-screen-mode-automated
|
||||
test('Share screen @ci', async ({ browser, page }) => {
|
||||
const screenshare = new ScreenShare(browser, page);
|
||||
await screenshare.init(true, true);
|
||||
|
@ -2,7 +2,9 @@ const { test } = require('@playwright/test');
|
||||
const { Language } = require('./language');
|
||||
|
||||
test.describe.parallel('Settings', () => {
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#application-settings
|
||||
test(`Locales`, async ({ browser, page }) => {
|
||||
test.slow();
|
||||
const language = new Language(browser, page);
|
||||
await language.init(true, true);
|
||||
await language.test();
|
||||
|
@ -1,7 +1,11 @@
|
||||
const { default: test } = require('@playwright/test');
|
||||
const Page = require('../core/page');
|
||||
const { getSettings } = require('../core/settings');
|
||||
const { startSharedNotes } = require('./util');
|
||||
const e = require('../core/elements');
|
||||
const { startSharedNotes, getNotesLocator, getShowMoreButtonLocator, getExportButtonLocator, getExportPlainTextLocator } = require('./util');
|
||||
const { expect } = require('@playwright/test');
|
||||
const { ELEMENT_WAIT_TIME } = require('../core/constants');
|
||||
const { sleep } = require('../core/helpers');
|
||||
|
||||
class SharedNotes extends Page {
|
||||
constructor(browser, page) {
|
||||
@ -13,6 +17,106 @@ class SharedNotes extends Page {
|
||||
test.fail(!sharedNotesEnabled, 'Shared notes is disabled');
|
||||
await startSharedNotes(this);
|
||||
}
|
||||
|
||||
async editMessage(notesLocator) {
|
||||
await this.down('Shift');
|
||||
let i = 7;
|
||||
while(i > 0) {
|
||||
await this.press('ArrowLeft');
|
||||
i--;
|
||||
}
|
||||
await this.up('Shift');
|
||||
await this.press('Backspace');
|
||||
i = 5;
|
||||
while(i > 0) {
|
||||
await this.press('ArrowLeft');
|
||||
i--;
|
||||
}
|
||||
await this.press('!');
|
||||
}
|
||||
|
||||
async typeInSharedNotes() {
|
||||
const { sharedNotesEnabled } = getSettings();
|
||||
test.fail(!sharedNotesEnabled, 'Shared notes is disabled');
|
||||
await startSharedNotes(this);
|
||||
const notesLocator = getNotesLocator(this);
|
||||
await notesLocator.type(e.message);
|
||||
this.editMessage(notesLocator);
|
||||
const editedMessage = '!Hello';
|
||||
await expect(notesLocator).toContainText(editedMessage, { timeout : ELEMENT_WAIT_TIME });
|
||||
}
|
||||
|
||||
async formatMessage(notesLocator) {
|
||||
|
||||
// U for '!'
|
||||
await this.down('Shift');
|
||||
await this.press('ArrowLeft');
|
||||
await this.up('Shift');
|
||||
await this.press('Control+U');
|
||||
await this.press('ArrowLeft');
|
||||
|
||||
// B for 'World'
|
||||
await this.down('Shift');
|
||||
let i = 5;
|
||||
while(i > 0) {
|
||||
await this.press('ArrowLeft');
|
||||
i--;
|
||||
}
|
||||
await this.up('Shift');
|
||||
await this.press('Control+B');
|
||||
await this.press('ArrowLeft');
|
||||
|
||||
await this.press('ArrowLeft');
|
||||
|
||||
// I for 'Hello'
|
||||
await this.down('Shift');
|
||||
i = 5;
|
||||
while(i > 0) {
|
||||
await this.press('ArrowLeft');
|
||||
i--;
|
||||
}
|
||||
await this.up('Shift');
|
||||
await this.press('Control+I');
|
||||
await this.press('ArrowLeft');
|
||||
}
|
||||
|
||||
async formatTextInSharedNotes() {
|
||||
const { sharedNotesEnabled } = getSettings();
|
||||
test.fail(!sharedNotesEnabled, 'Shared notes is disabled');
|
||||
await startSharedNotes(this);
|
||||
const notesLocator = getNotesLocator(this);
|
||||
await notesLocator.type(e.message);
|
||||
this.formatMessage(notesLocator);
|
||||
const html = await notesLocator.innerHTML();
|
||||
|
||||
const uText = '<u>!</u>';
|
||||
await expect(html.includes(uText)).toBeTruthy();
|
||||
|
||||
const bText = '<b>World</b>';
|
||||
await expect(html.includes(bText)).toBeTruthy();
|
||||
|
||||
const iText = '<i>Hello</i>'
|
||||
await expect(html.includes(bText)).toBeTruthy();
|
||||
}
|
||||
|
||||
async exportSharedNotes(testInfo) {
|
||||
const { sharedNotesEnabled } = getSettings();
|
||||
test.fail(!sharedNotesEnabled, 'Shared notes is disabled');
|
||||
await startSharedNotes(this);
|
||||
const notesLocator = getNotesLocator(this);
|
||||
await notesLocator.type(e.message);
|
||||
|
||||
const showMoreButtonLocator = getShowMoreButtonLocator(this);
|
||||
await showMoreButtonLocator.click();
|
||||
|
||||
const exportButtonLocator = getExportButtonLocator(this);
|
||||
await exportButtonLocator.click();
|
||||
|
||||
const exportPlainTextLocator = getExportPlainTextLocator(this);
|
||||
this.page.waitForEvent('download');
|
||||
await exportPlainTextLocator.click();
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
exports.SharedNotes = SharedNotes;
|
||||
|
@ -7,4 +7,22 @@ test.describe.parallel('Shared Notes', () => {
|
||||
await sharedNotes.init(true, true);
|
||||
await sharedNotes.openSharedNotes();
|
||||
});
|
||||
test('Type in shared notes', async ({ browser, page }) => {
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#using-shared-notes-panel
|
||||
const sharedNotes = new SharedNotes(browser, page);
|
||||
await sharedNotes.init(true, true);
|
||||
await sharedNotes.typeInSharedNotes();
|
||||
});
|
||||
test('Formate text in shared notes', async ({ browser, page }) => {
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#using-shared-notes-formatting-tools
|
||||
const sharedNotes = new SharedNotes(browser, page);
|
||||
await sharedNotes.init(true, true);
|
||||
await sharedNotes.formatTextInSharedNotes();
|
||||
});
|
||||
test('Export shared notes', async ({ browser, page }, testInfo) => {
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#exporting-shared-notes
|
||||
const sharedNotes = new SharedNotes(browser, page);
|
||||
await sharedNotes.init(true, true);
|
||||
await sharedNotes.exportSharedNotes(testInfo);
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
|
||||
const e = require('../core/elements');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
async function startSharedNotes(test) {
|
||||
await test.waitAndClick(e.sharedNotes);
|
||||
@ -14,5 +15,20 @@ function getNotesLocator(test) {
|
||||
.locator(e.etherpadEditable);
|
||||
}
|
||||
|
||||
function getShowMoreButtonLocator(test) {
|
||||
return test.page.frameLocator(e.etherpadFrame).locator(e.showMoreSharedNotesButton);
|
||||
}
|
||||
|
||||
function getExportButtonLocator(test) {
|
||||
return test.page.frameLocator(e.etherpadFrame).locator(e.exportSharedNotesButton);
|
||||
}
|
||||
|
||||
function getExportPlainTextLocator(test) {
|
||||
return test.page.frameLocator(e.etherpadFrame).locator(e.exportPlainButton);
|
||||
}
|
||||
|
||||
exports.startSharedNotes = startSharedNotes;
|
||||
exports.getNotesLocator = getNotesLocator;
|
||||
exports.getShowMoreButtonLocator = getShowMoreButtonLocator;
|
||||
exports.getExportButtonLocator = getExportButtonLocator;
|
||||
exports.getExportPlainTextLocator = getExportPlainTextLocator;
|
||||
|
@ -100,18 +100,25 @@ class Stress {
|
||||
|
||||
async usersJoinKeepingConnected() {
|
||||
const meetingId = await createMeeting(parameters);
|
||||
const pages = [];
|
||||
|
||||
for (let i = 1; i <= c.JOIN_TWO_USERS_KEEPING_CONNECTED_ROUNDS / 2; i++) {
|
||||
console.log(`joining ${i * 2} users of ${c.JOIN_TWO_USERS_KEEPING_CONNECTED_ROUNDS}`);
|
||||
const modPage = new Page(this.browser, await this.getNewPageTab());
|
||||
const userPage = new Page(this.browser, await this.getNewPageTab());
|
||||
Promise.all([
|
||||
pages.push(modPage);
|
||||
pages.push(userPage);
|
||||
await Promise.all([
|
||||
modPage.init(true, false, { meetingId, fullName: `Mod-${i}` }),
|
||||
userPage.init(false, false, { meetingId, fullName: `User-${i}` }),
|
||||
]);
|
||||
await modPage.waitForSelector(e.audioModal, c.ELEMENT_WAIT_LONGER_TIME);
|
||||
await userPage.waitForSelector(e.audioModal, c.ELEMENT_WAIT_LONGER_TIME);
|
||||
}
|
||||
|
||||
pages.forEach(async (currentPage) => {
|
||||
await currentPage.page.close();
|
||||
})
|
||||
}
|
||||
|
||||
async usersJoinExceddingParticipantsLimit() {
|
||||
|
@ -14,14 +14,20 @@ class LockViewers extends MultiUsers {
|
||||
|
||||
async lockShareWebcam() {
|
||||
await this.modPage.shareWebcam();
|
||||
await this.modPage.hasElement(e.webcamVideoItem);
|
||||
await this.userPage.hasElement(e.webcamVideoItem);
|
||||
await this.userPage2.hasElement(e.webcamVideoItem);
|
||||
await this.userPage.shareWebcam();
|
||||
await this.modPage.hasNElements(e.webcamVideoItem, 2);
|
||||
await this.userPage.hasNElements(e.webcamVideoItem, 2);
|
||||
await this.userPage2.hasNElements(e.webcamVideoItem, 2);
|
||||
await openLockViewers(this.modPage);
|
||||
await this.modPage.waitAndClickElement(e.lockShareWebcam);
|
||||
await this.modPage.waitAndClick(e.applyLockSettings);
|
||||
const videoContainerLockedCount = await this.userPage2.getSelectorCount(e.webcamVideoItem);
|
||||
expect(videoContainerLockedCount).toBe(1);
|
||||
await waitAndClearNotification(this.modPage);
|
||||
await this.modPage.wasNthElementRemoved(e.webcamVideoItem, 2);
|
||||
await this.userPage.wasNthElementRemoved(e.webcamVideoItem, 2);
|
||||
await this.userPage2.wasNthElementRemoved(e.webcamVideoItem, 2);
|
||||
|
||||
await this.userPage2.waitForSelector(e.dropdownWebcamButton);
|
||||
await this.userPage2.hasText(e.dropdownWebcamButton, this.modPage.username);
|
||||
|
@ -1,6 +1,6 @@
|
||||
const Page = require('../core/page');
|
||||
const { setStatus } = require('./util');
|
||||
const { waitAndClearNotification } = require('../notifications/util');
|
||||
const { waitAndClearNotification, waitAndClearDefaultPresentationNotification } = require('../notifications/util');
|
||||
const e = require('../core/elements');
|
||||
|
||||
class Status extends Page {
|
||||
@ -9,6 +9,7 @@ class Status extends Page {
|
||||
}
|
||||
|
||||
async changeUserStatus() {
|
||||
await waitAndClearDefaultPresentationNotification(this);
|
||||
await setStatus(this, e.applaud);
|
||||
await this.waitForSelector(e.smallToastMsg);
|
||||
await this.checkElement(e.applauseIcon);
|
||||
|
@ -9,7 +9,7 @@ const iPhone11 = devices['iPhone 11'];
|
||||
|
||||
test.describe.parallel('User', () => {
|
||||
test.describe.parallel('Actions', () => {
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#set-status--raise-hand-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#set-status--raise-hand-automated
|
||||
test('Raise and lower Hand Toast', async ({ browser, context, page }) => {
|
||||
const multiusers = new MultiUsers(browser, context);
|
||||
await multiusers.initModPage(page, true);
|
||||
@ -24,7 +24,7 @@ test.describe.parallel('User', () => {
|
||||
});
|
||||
|
||||
test.describe.parallel('List', () => {
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#set-status--raise-hand-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#set-status--raise-hand-automated
|
||||
test('Change user status @ci', async ({ browser, page }) => {
|
||||
const status = new Status(browser, page);
|
||||
await status.init(true, true);
|
||||
@ -37,12 +37,14 @@ test.describe.parallel('User', () => {
|
||||
await multiusers.userPresence();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#make-viewer-a-presenter-automated
|
||||
test('Make presenter @ci', async ({ browser, context, page }) => {
|
||||
const multiusers = new MultiUsers(browser, context);
|
||||
await multiusers.initPages(page);
|
||||
await multiusers.makePresenter();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#taking-presenter-status-back-automated
|
||||
test('Take presenter @ci', async ({ browser, context, page }) => {
|
||||
const multiusers = new MultiUsers(browser, context);
|
||||
await multiusers.initModPage(page);
|
||||
@ -86,6 +88,7 @@ test.describe.parallel('User', () => {
|
||||
});
|
||||
|
||||
test.describe.parallel('Lock viewers @ci', () => {
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#webcam
|
||||
test('Lock Share webcam', async ({ browser, context, page }) => {
|
||||
const lockViewers = new LockViewers(browser, context);
|
||||
await lockViewers.initPages(page);
|
||||
@ -93,6 +96,7 @@ test.describe.parallel('User', () => {
|
||||
await lockViewers.lockShareWebcam();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#see-other-viewers-webcams
|
||||
test('Lock See other viewers webcams', async ({ browser, context, page }) => {
|
||||
const lockViewers = new LockViewers(browser, context);
|
||||
await lockViewers.initPages(page);
|
||||
@ -100,6 +104,7 @@ test.describe.parallel('User', () => {
|
||||
await lockViewers.lockSeeOtherViewersWebcams();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#microphone
|
||||
test('Lock Share microphone', async ({ browser, context, page }) => {
|
||||
const lockViewers = new LockViewers(browser, context);
|
||||
await lockViewers.initPages(page);
|
||||
@ -107,6 +112,7 @@ test.describe.parallel('User', () => {
|
||||
await lockViewers.lockShareMicrophone();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#public-chat
|
||||
test('Lock Send public chat messages', async ({ browser, context, page }) => {
|
||||
const lockViewers = new LockViewers(browser, context);
|
||||
await lockViewers.initPages(page);
|
||||
@ -114,6 +120,7 @@ test.describe.parallel('User', () => {
|
||||
await lockViewers.lockSendPublicChatMessages();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#private-chat
|
||||
test('Lock Send private chat messages', async ({ browser, context, page }) => {
|
||||
const lockViewers = new LockViewers(browser, context);
|
||||
await lockViewers.initPages(page);
|
||||
@ -121,12 +128,14 @@ test.describe.parallel('User', () => {
|
||||
await lockViewers.lockSendPrivateChatMessages();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#shared-notes-1
|
||||
test('Lock Edit Shared Notes', async ({ browser, context, page }) => {
|
||||
const lockViewers = new LockViewers(browser, context);
|
||||
await lockViewers.initPages(page);
|
||||
await lockViewers.lockEditSharedNotes();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#see-other-viewers-in-the-users-list
|
||||
test('Lock See other viewers in the Users list', async ({ browser, context, page }) => {
|
||||
const lockViewers = new LockViewers(browser, context);
|
||||
await lockViewers.initPages(page);
|
||||
@ -134,6 +143,7 @@ test.describe.parallel('User', () => {
|
||||
await lockViewers.lockSeeOtherViewersUserList();
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#unlock-a-specific-user
|
||||
test('Unlock a user', async ({ browser, context, page }) => {
|
||||
const lockViewers = new LockViewers(browser, context);
|
||||
await lockViewers.initPages(page);
|
||||
@ -142,6 +152,7 @@ test.describe.parallel('User', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#saving-usernames
|
||||
test('Save user names', async ({ browser, context, page }, testInfo) => {
|
||||
const multiusers = new MultiUsers(browser, context);
|
||||
await multiusers.initPages(page);
|
||||
|
@ -2,6 +2,10 @@ const e = require('../core/elements');
|
||||
const { sleep } = require('../core/helpers');
|
||||
const { LOOP_INTERVAL, ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
|
||||
|
||||
// loop 5 times, every LOOP_INTERVAL milliseconds, and check that all
|
||||
// videos displayed are changing by comparing a hash of their
|
||||
// displayed contents
|
||||
|
||||
async function webcamContentCheck(test) {
|
||||
await test.waitForSelector(e.webcamVideoItem);
|
||||
await test.wasRemoved(e.webcamConnecting, ELEMENT_WAIT_LONGER_TIME);
|
||||
@ -9,30 +13,30 @@ async function webcamContentCheck(test) {
|
||||
let check;
|
||||
for (let i = repeats; i >= 1; i--) {
|
||||
console.log(`loop ${i}`);
|
||||
const checkCameras = () => {
|
||||
const checkCameras = async () => {
|
||||
const videos = document.querySelectorAll('video');
|
||||
const lastVideoColor = document.lastVideoColor || {};
|
||||
document.lastVideoColor = lastVideoColor;
|
||||
const lastVideoHash = document.lastVideoHash || {};
|
||||
document.lastVideoHash = lastVideoHash;
|
||||
|
||||
for (let v = 0; v < videos.length; v++) {
|
||||
const video = videos[v];
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
|
||||
const pixel = context.getImageData(0, 0, 1, 1).data;
|
||||
const pixelString = new Array(pixel).join(' ').toString();
|
||||
const pixel = context.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
|
||||
const pixelHash = await window.crypto.subtle.digest('SHA-1', pixel);
|
||||
|
||||
if (lastVideoColor[v]) {
|
||||
if (lastVideoColor[v] == pixelString) {
|
||||
if (lastVideoHash[v]) {
|
||||
if (lastVideoHash[v] == pixelHash) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
lastVideoColor[v] = pixelString;
|
||||
lastVideoHash[v] = pixelHash;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
check = await test.page.evaluate(checkCameras, i);
|
||||
check = await test.page.evaluate(checkCameras);
|
||||
if (!check) return false;
|
||||
await sleep(LOOP_INTERVAL);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ const { test } = require('@playwright/test');
|
||||
const { Webcam } = require('./webcam');
|
||||
|
||||
test.describe.parallel('Webcam @ci', () => {
|
||||
// https://docs.bigbluebutton.org/2.5/release-tests.html#joining-webcam-automated
|
||||
// https://docs.bigbluebutton.org/2.6/release-tests.html#joining-webcam-automated
|
||||
test('Shares webcam', async ({ browser, page }) => {
|
||||
const webcam = new Webcam(browser, page);
|
||||
await webcam.init(true, true);
|
||||
|
@ -417,7 +417,8 @@ endWhenNoModerator=false
|
||||
endWhenNoModeratorDelayInMinutes=1
|
||||
|
||||
# List of features to disable (comma-separated)
|
||||
# Available options: breakoutRooms, captions, chat, externalVideos, layouts, learningDashboard, polls, screenshare, sharedNotes, virtualBackgrounds
|
||||
# Available options: breakoutRooms, captions, chat, externalVideos, layouts, learningDashboard, polls, screenshare,
|
||||
# sharedNotes, virtualBackgrounds, downloadPresentationWithAnnotations, importPresentationWithAnnotationsFromBreakoutRooms
|
||||
#disabledFeatures=
|
||||
|
||||
# Notify users that recording is on
|
||||
|
@ -1,3 +1,3 @@
|
||||
. ./opts-global.sh
|
||||
|
||||
OPTS="$OPTS -t deb -d nodejs,npm,bbb-apps-akka,bbb-web"
|
||||
OPTS="$OPTS -t deb -d nodejs,npm,bbb-apps-akka,bbb-web,cairosvg,ghostscript,imagemagick"
|
||||
|
@ -375,18 +375,12 @@ module BigBlueButton
|
||||
[new_width, new_height]
|
||||
end
|
||||
|
||||
def self.pad_offset(video_width, video_height, area_width, area_height)
|
||||
pad_x = (2 * ((area_width - video_width).to_f / 4).round).to_i
|
||||
pad_y = (2 * ((area_height - video_height).to_f / 4).round).to_i
|
||||
[pad_x, pad_y]
|
||||
end
|
||||
|
||||
def self.composite_cut(output, cut, layout, videoinfo)
|
||||
duration = cut[:next_timestamp] - cut[:timestamp]
|
||||
BigBlueButton.logger.info " Cut start time #{cut[:timestamp]}, duration #{duration}"
|
||||
|
||||
ffmpeg_inputs = []
|
||||
ffmpeg_filter = "color=c=white:s=#{layout[:width]}x#{layout[:height]}:r=#{layout[:framerate]}"
|
||||
|
||||
layout[:areas].each do |layout_area|
|
||||
area = cut[:areas][layout_area[:name]]
|
||||
video_count = area.length
|
||||
@ -443,9 +437,6 @@ module BigBlueButton
|
||||
scale_width, scale_height = aspect_scale(video_width, video_height, tile_width, tile_height)
|
||||
BigBlueButton.logger.debug " scaled size: #{scale_width}x#{scale_height}"
|
||||
|
||||
offset_x, offset_y = pad_offset(scale_width, scale_height, tile_width, tile_height)
|
||||
BigBlueButton.logger.debug " offset: left: #{offset_x}, top: #{offset_y}"
|
||||
|
||||
BigBlueButton.logger.debug(" start timestamp: #{video[:timestamp]}")
|
||||
seek_offset = this_videoinfo[:start_time]
|
||||
BigBlueButton.logger.debug(" seek offset: #{seek_offset}")
|
||||
@ -500,31 +491,30 @@ module BigBlueButton
|
||||
if seek > 0
|
||||
seek = seek + seek_offset
|
||||
end
|
||||
ffmpeg_filter << "movie=#{video[:filename]}:sp=#{ms_to_s(seek)}"
|
||||
input_index = ffmpeg_inputs.length
|
||||
ffmpeg_inputs << {
|
||||
filename: video[:filename],
|
||||
seek: seek,
|
||||
}
|
||||
ffmpeg_filter << "[#{input_index}]"
|
||||
# Scale the video length for the deskshare timestamp workaround
|
||||
if !scale.nil?
|
||||
ffmpeg_filter << ",setpts=PTS*#{scale}"
|
||||
ffmpeg_filter << "setpts=PTS*#{scale},"
|
||||
end
|
||||
# Subtract away the offset from the timestamps, so the trimming
|
||||
# in the fps filter is accurate
|
||||
ffmpeg_filter << ",setpts=PTS-#{ms_to_s(seek_offset)}/TB"
|
||||
ffmpeg_filter << "setpts=PTS-#{ms_to_s(seek_offset)}/TB"
|
||||
# fps filter fills in frames up to the desired start point, and
|
||||
# cuts the video there
|
||||
ffmpeg_filter << ",fps=#{layout[:framerate]}:start_time=#{ms_to_s(video[:timestamp])}"
|
||||
# Reset the timestamps to start at 0 so that everything is synced
|
||||
# for the video tiling, and scale to the desired size.
|
||||
ffmpeg_filter << ",setpts=PTS-STARTPTS,scale=#{scale_width}:#{scale_height},setsar=1"
|
||||
ffmpeg_filter << ",setpts=PTS-STARTPTS,scale=w=#{tile_width}:h=#{tile_height}:force_original_aspect_ratio=decrease,setsar=1"
|
||||
# And finally, pad the video to the desired aspect ratio
|
||||
ffmpeg_filter << ",pad=w=#{tile_width}:h=#{tile_height}:x=#{offset_x}:y=#{offset_y}:color=white"
|
||||
ffmpeg_filter << "[#{pad_name}_movie];"
|
||||
|
||||
# In case the video was shorter than expected, we might have to pad
|
||||
# it to length. do that by concatenating a video generated by the
|
||||
# color filter. (It would be nice to repeat the last frame instead,
|
||||
# but there's no easy way to do that.)
|
||||
ffmpeg_filter << "color=c=white:s=#{tile_width}x#{tile_height}:r=#{layout[:framerate]}"
|
||||
ffmpeg_filter << "[#{pad_name}_pad];"
|
||||
ffmpeg_filter << "[#{pad_name}_movie][#{pad_name}_pad]concat=n=2:v=1:a=0[#{pad_name}];"
|
||||
ffmpeg_filter << ",pad=w=#{tile_width}:h=#{tile_height}:x=-1:y=-1:color=white"
|
||||
# Extend the video to the desired length
|
||||
ffmpeg_filter << ",tpad=stop=-1:stop_mode=add:color=white"
|
||||
ffmpeg_filter << "[#{pad_name}];"
|
||||
end
|
||||
|
||||
# Create the video rows
|
||||
@ -557,7 +547,21 @@ module BigBlueButton
|
||||
|
||||
ffmpeg_filter << ",trim=end=#{ms_to_s(duration)}"
|
||||
|
||||
ffmpeg_cmd = [*FFMPEG, '-filter_complex', ffmpeg_filter, '-an', *FFMPEG_WF_ARGS, '-r', layout[:framerate].to_s, output]
|
||||
ffmpeg_cmd = [*FFMPEG]
|
||||
ffmpeg_inputs.each do |input|
|
||||
ffmpeg_cmd += ['-ss', ms_to_s(input[:seek]), '-i', input[:filename]]
|
||||
end
|
||||
|
||||
BigBlueButton.logger.debug(' ffmpeg filter_complex_script:')
|
||||
BigBlueButton.logger.debug(ffmpeg_filter)
|
||||
filter_complex_script = "#{output}.filter"
|
||||
File.open(filter_complex_script, 'w') do |io|
|
||||
io.write(ffmpeg_filter)
|
||||
end
|
||||
ffmpeg_cmd += ['-filter_complex_script', filter_complex_script]
|
||||
|
||||
ffmpeg_cmd += ['-an', *FFMPEG_WF_ARGS, '-r', layout[:framerate].to_s, output]
|
||||
|
||||
exitstatus = BigBlueButton.exec_ret(*ffmpeg_cmd)
|
||||
raise "ffmpeg failed, exit code #{exitstatus}" if exitstatus != 0
|
||||
|
||||
|
@ -39,7 +39,7 @@ filepathPresOverride = "/etc/bigbluebutton/recording/presentation.yml"
|
||||
hasOverride = File.file?(filepathPresOverride)
|
||||
if (hasOverride)
|
||||
presOverrideProps = YAML::load(File.open(filepathPresOverride))
|
||||
$presentation_props = $presentation_props.merge(presOverrideProps)
|
||||
@presentation_props = @presentation_props.merge(presOverrideProps)
|
||||
end
|
||||
|
||||
# There's a couple of places where stuff is mysteriously divided or multiplied
|
||||
|
Loading…
Reference in New Issue
Block a user