[apply-toast-shared-notes] - changes in review and resolve merge conflict

This commit is contained in:
GuiLeme 2022-08-11 15:50:06 -03:00
commit 491380096e
63 changed files with 3430 additions and 1100 deletions

View File

@ -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) {

View File

@ -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,

View 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,
},
};

View File

@ -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,

View File

@ -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();
})();

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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);
})();

View File

@ -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

View File

@ -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" />

View 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,
};

View File

@ -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) {

View File

@ -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;

View File

@ -29,6 +29,8 @@ const ActionsBarContainer = (props) => {
const amIPresenter = users[Auth.meetingID][Auth.userID].presenter;
if (actionsBarStyle.display === false) return null;
return (
<ActionsBar {
...{

View File

@ -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 />

View File

@ -9,6 +9,9 @@ const ClosedCaptionToggleButton = styled(Button)`
background-color: transparent !important;
border-color: ${colorWhite} !important;
}
i {
margin-top: .4rem;
}
`}
`;

View File

@ -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

View File

@ -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}

View File

@ -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) => {

View File

@ -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,

View File

@ -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

View File

@ -169,7 +169,6 @@ class Presentation extends PureComponent {
} = this.props;
const { presentationWidth, presentationHeight } = this.state;
const {
numCameras: prevNumCameras,
presentationBounds: prevPresentationBounds,

View File

@ -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);

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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>
);
};

View File

@ -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,
};

View File

@ -134,7 +134,7 @@ const SidebarContent = (props) => {
<ErrorBoundary
Fallback={FallbackView}
>
<ChatContainer />
<ChatContainer width={width}/>
</ErrorBoundary>
)}
{sidebarContentPanel === PANELS.SHARED_NOTES && <NotesContainer />}

View File

@ -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);
}
}
`}

View File

@ -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)}
/>
);

View File

@ -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);

View File

@ -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 }) => {

View File

@ -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;
}

View File

@ -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,
};

View File

@ -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",

View File

@ -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",

View File

@ -507,6 +507,8 @@ public:
moderatorChatEmphasized: true
autoConvertEmoji: true
enableEmojiPicker: false
# e.g.: disableEmojis: ['1F595','1F922']
disableEmojis: []
notes:
enabled: true
id: notes

View File

@ -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",

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -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"]';

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -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;

View File

@ -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);
});
});

View File

@ -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;

View File

@ -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() {

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);
}

View File

@ -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);

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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