Resolve conflicts

This commit is contained in:
Gustavo Trott 2023-08-30 15:31:32 -03:00
commit 5eb04aab91
91 changed files with 1075 additions and 404 deletions

View File

@ -43,9 +43,10 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildNewPresFileAvailable(fileURI: String, presId: String, typeOfExport: String): NewPresFileAvailableMsg = {
def buildNewPresFileAvailable(annotatedFileURI: String, originalFileURI: String, convertedFileURI: String,
presId: String, fileStateType: String): NewPresFileAvailableMsg = {
val header = BbbClientMsgHeader(NewPresFileAvailableMsg.NAME, "not-used", "not-used")
val body = NewPresFileAvailableMsgBody(fileURI, presId, typeOfExport)
val body = NewPresFileAvailableMsgBody(annotatedFileURI, originalFileURI, convertedFileURI, presId, fileStateType)
NewPresFileAvailableMsg(header, body)
}
@ -54,8 +55,12 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, "not-used")
val envelope = BbbCoreEnvelope(PresentationPageConvertedEventMsg.NAME, routing)
val header = BbbClientMsgHeader(NewPresFileAvailableEvtMsg.NAME, liveMeeting.props.meetingProp.intId, "not-used")
val body = NewPresFileAvailableEvtMsgBody(fileURI = newPresFileAvailableMsg.body.fileURI, presId = newPresFileAvailableMsg.body.presId,
typeOfExport = newPresFileAvailableMsg.body.typeOfExport)
val body = NewPresFileAvailableEvtMsgBody(
annotatedFileURI = newPresFileAvailableMsg.body.annotatedFileURI,
originalFileURI = newPresFileAvailableMsg.body.originalFileURI,
convertedFileURI = newPresFileAvailableMsg.body.convertedFileURI, presId = newPresFileAvailableMsg.body.presId,
fileStateType = newPresFileAvailableMsg.body.fileStateType
)
val event = NewPresFileAvailableEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
@ -148,9 +153,9 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
val exportJob: ExportJob = new ExportJob(jobId, JobTypes.DOWNLOAD, "annotated_slides", presId, presLocation, allPages, pagesRange, meetingId, "");
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
val isOriginalPresentationType = m.body.typeOfExport == "Original"
val isPresentationOriginalOrConverted = m.body.fileStateType == "Original" || m.body.fileStateType == "Converted"
if (!isOriginalPresentationType) {
if (!isPresentationOriginalOrConverted) {
// Send Export Job to Redis
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
bus.outGW.send(job)
@ -161,13 +166,18 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
} else {
// Return existing uploaded file directly
val convertedFileName = currentPres.get.filenameConverted
val filename = if (convertedFileName == "") currentPres.get.name else convertedFileName
val presFilenameExt = filename.split("\\.").last
val originalFilename = currentPres.get.name
val originalFileExt = originalFilename.split("\\.").last
val convertedFileExt = if (convertedFileName != "") convertedFileName.split("\\.").last else ""
PresentationSender.broadcastSetPresentationDownloadableEvtMsg(bus, meetingId, "DEFAULT_PRESENTATION_POD", "not-used", presId, true, filename)
val convertedFileURI = if (convertedFileName != "") List("presentation", "download", meetingId,
s"${presId}?presFilename=${presId}.${convertedFileExt}&filename=${convertedFileName}").mkString("", File.separator, "")
else ""
val originalFileURI = List("presentation", "download", meetingId,
s"${presId}?presFilename=${presId}.${originalFileExt}&filename=${originalFilename}").mkString("", File.separator, "")
val fileURI = List("presentation", "download", meetingId, s"${presId}?presFilename=${presId}.${presFilenameExt}&filename=${filename}").mkString("", File.separator, "")
val event = buildNewPresFileAvailable(fileURI, presId, m.body.typeOfExport)
val event = buildNewPresFileAvailable("", originalFileURI, convertedFileURI, presId,
m.body.fileStateType)
handle(event, liveMeeting, bus)
}
@ -226,14 +236,19 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
}
def handle(m: NewPresFileAvailableMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
log.info("Received NewPresFileAvailableMsg meetingId={} presId={} fileUrl={}", liveMeeting.props.meetingProp.intId, m.body.presId, m.body.fileURI)
log.info(
"Received NewPresFileAvailableMsg meetingId={} presId={}",
liveMeeting.props.meetingProp.intId, m.body.presId
)
//TODO let frontend choose the name in favor of internationalization
val presentationDownloadInfo = Map(
"fileURI" -> m.body.fileURI,
"filename" -> "annotated_slides.pdf"
)
ChatMessageDAO.insertSystemMsg(liveMeeting.props.meetingProp.intId, GroupChatApp.MAIN_PUBLIC_CHAT, "", "presentation", presentationDownloadInfo, "")
if (m.body.fileStateType == "Annotated") {
val presentationDownloadInfo = Map(
"fileURI" -> m.body.annotatedFileURI,
"filename" -> "annotated_slides.pdf"
)
ChatMessageDAO.insertSystemMsg(liveMeeting.props.meetingProp.intId, GroupChatApp.MAIN_PUBLIC_CHAT, "", "presentation", presentationDownloadInfo, "")
}
bus.outGW.send(buildBroadcastNewPresFileAvailable(m, liveMeeting))
}

View File

@ -34,6 +34,8 @@ trait PresentationConversionCompletedSysPubMsgHdlr {
msg.body.code,
presVO,
)
val originalDownloadableExtension = pres.name.split("\\.").last
PresentationSender.broadcastSetPresentationDownloadableEvtMsg(
bus,
meetingId,
@ -41,7 +43,8 @@ trait PresentationConversionCompletedSysPubMsgHdlr {
msg.header.userId,
pres.id,
pres.downloadable,
pres.name
pres.name,
originalDownloadableExtension
)
val presWithConvertedName = PresentationInPod(pres.id, pres.name, pres.current, pres.pages,

View File

@ -8,10 +8,12 @@ object PresentationSender {
def broadcastSetPresentationDownloadableEvtMsg(
bus: MessageBus,
meetingId: String,
podId: String, userId: String,
podId: String,
userId: String,
presentationId: String,
downloadable: Boolean,
presFilename: String,
downloadableExtension: String
): Unit = {
val routing = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
@ -20,12 +22,11 @@ object PresentationSender {
val envelope = BbbCoreEnvelope(SetPresentationDownloadableEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(SetPresentationDownloadableEvtMsg.NAME, meetingId, userId)
val body = SetPresentationDownloadableEvtMsgBody(podId, presentationId, downloadable, presFilename)
val body = SetPresentationDownloadableEvtMsgBody(podId, presentationId, downloadable, presFilename, downloadableExtension)
val event = SetPresentationDownloadableEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
def broadcastPresentationConversionCompletedEvtMsg(
bus: MessageBus,
meetingId: String,

View File

@ -31,8 +31,11 @@ trait SetPresentationDownloadablePubMsgHdlr extends RightsManagementTrait {
pod <- PresentationPodsApp.getPresentationPod(state, podId)
pres <- pod.getPresentation(presentationId)
} yield {
val downloadableExtension = if (msg.body.fileStateType == "Original")
pres.name.split("\\.").last else pres.filenameConverted.split("\\.").last
PresentationSender.broadcastSetPresentationDownloadableEvtMsg(bus, meetingId, pod.id,
msg.header.userId, presentationId, downloadable, pres.name)
msg.header.userId, presentationId, downloadable, pres.name, downloadableExtension)
val pods = state.presentationPodManager.setPresentationDownloadableInPod(pod.id, presentationId, downloadable)
state.update(pods)
@ -46,4 +49,4 @@ trait SetPresentationDownloadablePubMsgHdlr extends RightsManagementTrait {
}
}
}
}

View File

@ -12,11 +12,12 @@ case class PreuploadedPresentationsSysPubMsgBody(presentations: Vector[Presentat
object MakePresentationDownloadReqMsg { val NAME = "MakePresentationDownloadReqMsg" }
case class MakePresentationDownloadReqMsg(header: BbbClientMsgHeader, body: MakePresentationDownloadReqMsgBody) extends StandardMsg
case class MakePresentationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int], typeOfExport: String)
case class MakePresentationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int], fileStateType: String)
object NewPresFileAvailableMsg { val NAME = "NewPresFileAvailableMsg" }
case class NewPresFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresFileAvailableMsgBody) extends StandardMsg
case class NewPresFileAvailableMsgBody(fileURI: String, presId: String, typeOfExport: String)
case class NewPresFileAvailableMsgBody(annotatedFileURI: String, originalFileURI: String, convertedFileURI: String,
presId: String, fileStateType: String)
object PresAnnStatusMsg { val NAME = "PresAnnStatusMsg" }
case class PresAnnStatusMsg(header: BbbClientMsgHeader, body: PresAnnStatusMsgBody) extends StandardMsg
@ -39,7 +40,8 @@ case class NewPresentationEvtMsgBody(presentation: PresentationVO)
object NewPresFileAvailableEvtMsg { val NAME = "NewPresFileAvailableEvtMsg" }
case class NewPresFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresFileAvailableEvtMsgBody) extends BbbCoreMsg
case class NewPresFileAvailableEvtMsgBody(fileURI: String, presId: String, typeOfExport: String)
case class NewPresFileAvailableEvtMsgBody(annotatedFileURI: String, originalFileURI: String, convertedFileURI: String,
presId: String, fileStateType: String)
object PresAnnStatusEvtMsg { val NAME = "PresAnnStatusEvtMsg" }
case class PresAnnStatusEvtMsg(header: BbbClientMsgHeader, body: PresAnnStatusEvtMsgBody) extends BbbCoreMsg

View File

@ -33,7 +33,8 @@ case class RemovePresentationPubMsgBody(podId: String, presentationId: String)
object SetPresentationDownloadablePubMsg { val NAME = "SetPresentationDownloadablePubMsg" }
case class SetPresentationDownloadablePubMsg(header: BbbClientMsgHeader, body: SetPresentationDownloadablePubMsgBody) extends StandardMsg
case class SetPresentationDownloadablePubMsgBody(podId: String, presentationId: String, downloadable: Boolean)
case class SetPresentationDownloadablePubMsgBody(podId: String, presentationId: String, downloadable: Boolean,
fileStateType: String)
object ResizeAndMovePagePubMsg { val NAME = "ResizeAndMovePagePubMsg" }
case class ResizeAndMovePagePubMsg(header: BbbClientMsgHeader, body: ResizeAndMovePagePubMsgBody) extends StandardMsg
@ -336,7 +337,8 @@ case class RemovePresentationEvtMsgBody(podId: String, presentationId: String)
object SetPresentationDownloadableEvtMsg { val NAME = "SetPresentationDownloadableEvtMsg" }
case class SetPresentationDownloadableEvtMsg(header: BbbClientMsgHeader, body: SetPresentationDownloadableEvtMsgBody) extends BbbCoreMsg
case class SetPresentationDownloadableEvtMsgBody(podId: String, presentationId: String, downloadable: Boolean, presFilename: String)
case class SetPresentationDownloadableEvtMsgBody(podId: String, presentationId: String, downloadable: Boolean,
presFilename: String, downloadableExtension: String)
object ResizeAndMovePageEvtMsg { val NAME = "ResizeAndMovePageEvtMsg" }
case class ResizeAndMovePageEvtMsg(header: BbbClientMsgHeader, body: ResizeAndMovePageEvtMsgBody) extends BbbCoreMsg

View File

@ -105,21 +105,33 @@ public final class Util {
return path;
}
public static File getPresFileDownloadMarker(File presBaseDir, String presId) {
public static File getPresFileDownloadMarker(File presBaseDir, String presId, String downloadableExtension) {
if (presBaseDir != null) {
String downloadMarker = presId.concat(".downloadable");
String downloadMarker = presId.concat(".").concat(downloadableExtension).concat(".downloadable");
return new File(presBaseDir.getAbsolutePath() + File.separatorChar + downloadMarker);
}
return null;
}
public static void deleteAllDownloadableMarksInPresentations(File presFileDir) {
// Delete files with .downloadable at the end of its filename
File[] presFiles = presFileDir.listFiles();
for (File presFile : presFiles) {
if (presFile.isFile() && presFile.getName().endsWith(".downloadable")) {
presFile.delete();
}
}
}
public static void makePresentationDownloadable(
File presFileDir,
String presId,
boolean downloadable
boolean downloadable,
String downloadableExtension
) throws IOException {
File downloadMarker = Util.getPresFileDownloadMarker(presFileDir, presId);
File downloadMarker = Util.getPresFileDownloadMarker(presFileDir, presId, downloadableExtension);
if (downloadable && downloadMarker != null && ! downloadMarker.exists()) {
Util.deleteAllDownloadableMarksInPresentations(presFileDir);
downloadMarker.createNewFile();
} else if (!downloadable && downloadMarker != null && downloadMarker.exists()) {
downloadMarker.delete();

View File

@ -5,11 +5,14 @@ public class MakePresentationDownloadableMsg implements IMessage {
public final String presId;
public final String presFilename;
public final Boolean downloadable;
public final String downloadableExtension;
public MakePresentationDownloadableMsg(String meetingId, String presId, String presFilename, Boolean downloadable) {
public MakePresentationDownloadableMsg(String meetingId, String presId, String presFilename,
Boolean downloadable, String downloadableExtension) {
this.meetingId = meetingId;
this.presId = presId;
this.presFilename = presFilename;
this.downloadable = downloadable;
this.downloadableExtension = downloadableExtension;
}
}

View File

@ -78,7 +78,7 @@ public class RecordingServiceFileImpl implements RecordingService {
public void processMakePresentationDownloadableMsg(MakePresentationDownloadableMsg msg) {
try {
File presDir = Util.getPresentationDir(presentationBaseDir, msg.meetingId, msg.presId);
Util.makePresentationDownloadable(presDir, msg.presId, msg.downloadable);
Util.makePresentationDownloadable(presDir, msg.presId, msg.downloadable, msg.downloadableExtension);
} catch (IOException e) {
log.error("Failed to make presentation downloadable: {}", e);
}
@ -96,7 +96,7 @@ public class RecordingServiceFileImpl implements RecordingService {
String presFilenameExt = FilenameUtils.getExtension(presFilename);
File presDir = Util.getPresentationDir(presentationBaseDir, meetingId, presId);
File downloadMarker = Util.getPresFileDownloadMarker(presDir, presId);
File downloadMarker = Util.getPresFileDownloadMarker(presDir, presId, presFilenameExt);
if (presDir != null && downloadMarker != null && downloadMarker.exists()) {
String safePresFilename = presId.concat(".").concat(presFilenameExt);
File presFile = new File(presDir.getAbsolutePath() + File.separatorChar + safePresFilename);

View File

@ -62,7 +62,7 @@ public final class SupportedFileTypes {
add(JPEG); add(JPG); add(PNG);
}
});
/*
* Returns if the file with extension is supported.
*/

View File

@ -227,14 +227,4 @@ public final class UploadedPresentation {
String nameWithoutExtension = FilenameUtils.removeExtension(name);
this.filenameConverted = nameWithoutExtension.concat("." + newExtension);
}
public void deleteOriginalFile() {
String pathToFileWithoutExtension = FilenameUtils.removeExtension(uploadedFile.getPath());
String newExtension = FilenameUtils.getExtension(uploadedFile.getPath());
String originalExtension = FilenameUtils.getExtension(name);
if (!originalExtension.equals("pdf") && newExtension.equals("pdf")) {
File originalFile = new File(pathToFileWithoutExtension + "." + originalExtension);
originalFile.delete();
}
}
}

View File

@ -1,6 +1,7 @@
package org.bigbluebutton.presentation.imp;
import com.google.gson.Gson;
import org.apache.commons.io.FilenameUtils;
import org.bigbluebutton.api.Util;
import org.bigbluebutton.presentation.*;
import org.bigbluebutton.presentation.messages.*;
@ -65,7 +66,15 @@ public class PresentationFileProcessor {
private void processMakePresentationDownloadableMsg(UploadedPresentation pres) {
try {
File presentationFileDir = pres.getUploadedFile().getParentFile();
Util.makePresentationDownloadable(presentationFileDir, pres.getId(), pres.isDownloadable());
if (!pres.getFilenameConverted().equals("")) {
String fileExtensionConverted = FilenameUtils.getExtension(pres.getFilenameConverted());
Util.makePresentationDownloadable(presentationFileDir, pres.getId(), pres.isDownloadable(),
fileExtensionConverted);
}
String fileExtensionOriginal = FilenameUtils.getExtension(pres.getName());
Util.makePresentationDownloadable(presentationFileDir, pres.getId(), pres.isDownloadable(),
fileExtensionOriginal);
} catch (IOException e) {
log.error("Failed to make presentation downloadable: {}", e);
}

View File

@ -108,9 +108,6 @@ public class SlidesGenerationProgressNotifier {
log.error("GeneratedSlidesInfoHelper was not set. Could not notify interested listeners.");
return;
}
// Completed conversion -> delete original file
pres.deleteOriginalFile();
DocPageCompletedProgress progress = new DocPageCompletedProgress(pres.getPodId(), pres.getMeetingId(),
pres.getId(), pres.getTemporaryPresentationId(), pres.getId(),
pres.getName(), "notUsedYet", "notUsedYet",

View File

@ -79,7 +79,7 @@ object MsgBuilder {
val pngUrl = presBaseUrl + "/png/" + page
val urls = Map("thumb" -> thumbUrl, "text" -> txtUrl, "svg" -> svgUrl, "png" -> pngUrl)
try {
val imgUrl = new URL(svgUrl)
val imgContent = XML.load(imgUrl)

View File

@ -187,7 +187,8 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
val presId = msg.body.presentationId
val downloadable = msg.body.downloadable
val presFilename = msg.body.presFilename
val m = new MakePresentationDownloadableMsg(meetingId, presId, presFilename, downloadable)
val downloadableExtension = msg.body.downloadableExtension;
val m = new MakePresentationDownloadableMsg(meetingId, presId, presFilename, downloadable, downloadableExtension)
olgMsgGW.handle(m)
}

View File

@ -70,9 +70,11 @@ class NewPresFileAvailableMsg {
userId: '',
},
body: {
fileURI: link,
annotatedFileURI: link,
originalFileURI: '',
convertedFileURI: '',
presId: exportJob.presId,
typeOfExport: "Annotated",
fileStateType: 'Annotated',
},
},
};

View File

@ -4,13 +4,17 @@ import setPresentationDownloadable from '../modifiers/setPresentationDownloadabl
export default async function handlePresentationDownloadableSet({ body }, meetingId) {
check(body, Object);
const { presentationId, podId, downloadable } = body;
const {
presentationId, podId, downloadable, downloadableExtension,
} = body;
check(meetingId, String);
check(presentationId, String);
check(podId, String);
check(downloadable, Boolean);
check(downloadableExtension, String);
const result = await setPresentationDownloadable(meetingId, podId, presentationId, downloadable);
const result = await setPresentationDownloadable(meetingId, podId, presentationId, downloadable,
downloadableExtension);
return result;
}

View File

@ -7,16 +7,36 @@ export default async function handlePresentationExport({ body }, meetingId) {
check(body, Object);
check(meetingId, String);
const { fileURI, presId, typeOfExport } = body;
const {
annotatedFileURI,
originalFileURI,
convertedFileURI,
presId,
fileStateType,
} = body;
check(fileURI, String);
check(annotatedFileURI, String);
check(originalFileURI, String);
check(convertedFileURI, String);
check(presId, String);
check(typeOfExport, String);
check(fileStateType, String);
if (typeOfExport === 'Original') {
await setOriginalUriDownload(meetingId, presId, fileURI);
if (fileStateType === 'Original' || fileStateType === 'Converted') {
if (fileStateType === 'Converted') {
await setOriginalUriDownload(
meetingId,
presId,
convertedFileURI,
);
} else {
await setOriginalUriDownload(
meetingId,
presId,
originalFileURI,
);
}
} else {
await sendExportedPresentationChatMsg(meetingId, presId, fileURI, typeOfExport);
await sendExportedPresentationChatMsg(meetingId, presId, annotatedFileURI, fileStateType);
}
await setPresentationExporting(meetingId, presId, { status: 'EXPORTED' });
}

View File

@ -4,7 +4,7 @@ import Presentations from '/imports/api/presentations';
const DEFAULT_FILENAME = 'annotated_slides.pdf';
export default async function sendExportedPresentationChatMsg(meetingId,
presentationId, fileURI, typeOfExport) {
presentationId, fileURI, fileStateType) {
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
const PUBLIC_CHAT_SYSTEM_ID = CHAT_CONFIG.system_userid;
@ -18,7 +18,7 @@ export default async function sendExportedPresentationChatMsg(meetingId,
type: 'presentation',
fileURI,
filename: pres?.name || DEFAULT_FILENAME,
typeOfExport,
fileStateType,
};
const payload = {

View File

@ -7,7 +7,7 @@ import Presentations from '/imports/api/presentations';
const EXPORTING_THRESHOLD_PER_SLIDE = 2500;
export default async function exportPresentation(presentationId, type) {
export default async function exportPresentation(presentationId, fileStateType) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'MakePresentationDownloadReqMsg';
@ -22,7 +22,7 @@ export default async function exportPresentation(presentationId, type) {
const payload = {
presId: presentationId,
allPages: true,
typeOfExport: type,
fileStateType,
pages: [],
};

View File

@ -3,7 +3,7 @@ import { check } from 'meteor/check';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default function setPresentationDownloadable(presentationId, downloadable) {
export default function setPresentationDownloadable(presentationId, downloadable, fileStateType) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'SetPresentationDownloadablePubMsg';
@ -15,11 +15,13 @@ export default function setPresentationDownloadable(presentationId, downloadable
check(requesterUserId, String);
check(downloadable, Match.Maybe(Boolean));
check(presentationId, String);
check(fileStateType, Match.Maybe(String));
const payload = {
presentationId,
podId: 'DEFAULT_PRESENTATION_POD',
downloadable,
fileStateType,
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);

View File

@ -3,11 +3,12 @@ import Presentations from '/imports/api/presentations';
import Logger from '/imports/startup/server/logger';
export default async function setPresentationDownloadable(meetingId, podId,
presentationId, downloadable) {
presentationId, downloadable, extensionToBeDownloadable) {
check(meetingId, String);
check(presentationId, String);
check(podId, String);
check(downloadable, Boolean);
check(extensionToBeDownloadable, String);
const selector = {
meetingId,
@ -15,9 +16,14 @@ export default async function setPresentationDownloadable(meetingId, podId,
id: presentationId,
};
let downloadableExtension = extensionToBeDownloadable;
if (!downloadable) {
downloadableExtension = '';
}
const modifier = {
$set: {
downloadable,
downloadableExtension,
},
};

View File

@ -280,7 +280,7 @@ class ActionsDropdown extends PureComponent {
});
}
if (isCameraAsContentEnabled && amIPresenter && !isMobile) {
if (isCameraAsContentEnabled && amIPresenter) {
actions.push({
icon: hasCameraAsContent ? 'video_off' : 'video',
label: hasCameraAsContent

View File

@ -344,14 +344,14 @@ const removePackagedClassAttribute = (classnames, attribute) => {
});
};
const getExportedPresentationString = (fileURI, filename, intl, typeOfExport) => {
const intlTypeOfExport = typeOfExport === 'Original' ? intlMessages.original : intlMessages.currentState;
const getExportedPresentationString = (fileURI, filename, intl, fileStateType) => {
const intlFileStateType = fileStateType === 'Original' ? intlMessages.original : intlMessages.currentState;
const href = `${APP.bbbWebBase}/${fileURI}`;
const warningIcon = '<i class="icon-bbb-warning"></i>';
const label = `<span>${intl.formatMessage(intlMessages.download)}</span>`;
const notAccessibleWarning = `<span title="${intl.formatMessage(intlMessages.notAccessibleWarning)}">${warningIcon}</span>`;
const link = `<a aria-label="${intl.formatMessage(intlMessages.notAccessibleWarning)}" href=${href} type="application/pdf" target="_blank" rel="noopener, noreferrer" download>${label}&nbsp;${notAccessibleWarning}</a>`;
const name = `<span>${filename} (${intl.formatMessage(intlTypeOfExport)})</span>`;
const name = `<span>${filename} (${intl.formatMessage(intlFileStateType)})</span>`;
return `${name}</br>${link}`;
};

View File

@ -480,7 +480,7 @@ class TimeWindowChatItem extends PureComponent {
type="presentation"
key={messages[0].id}
text={getExportedPresentationString(extra.fileURI,
extra.filename, intl, extra.typeOfExport)}
extra.filename, intl, extra.fileStateType)}
time={messages[0].time}
chatAreaId={chatAreaId}
lastReadMessageTime={lastReadMessageTime}

View File

@ -97,6 +97,12 @@ class PresentationToolbar extends PureComponent {
constructor(props) {
super(props);
this.state = {
wasFTWActive: false,
};
this.setWasActive = this.setWasActive.bind(this);
this.handleFTWSlideChange = this.handleFTWSlideChange.bind(this);
this.handleSkipToSlideChange = this.handleSkipToSlideChange.bind(this);
this.change = this.change.bind(this);
this.renderAriaDescs = this.renderAriaDescs.bind(this);
@ -112,18 +118,40 @@ class PresentationToolbar extends PureComponent {
}
componentDidUpdate(prevProps) {
const { zoom, setIsPanning, fitToWidth } = this.props;
const { zoom, setIsPanning, fitToWidth, fitToWidthHandler, currentSlideNum } = this.props;
const { wasFTWActive } = this.state;
if (zoom <= HUNDRED_PERCENT && zoom !== prevProps.zoom && !fitToWidth) setIsPanning();
if ((prevProps?.currentSlideNum !== currentSlideNum) && (!fitToWidth && wasFTWActive)) {
setTimeout(() => {
fitToWidthHandler();
this.setWasActive(false);
}, 150)
}
}
componentWillUnmount() {
document.removeEventListener('keydown', this.switchSlide);
}
setWasActive(wasFTWActive) {
this.setState({ wasFTWActive });
}
handleFTWSlideChange() {
const { fitToWidth, fitToWidthHandler } = this.props;
if (fitToWidth) {
fitToWidthHandler();
this.setWasActive(fitToWidth);
}
}
handleSkipToSlideChange(event) {
const { skipToSlide, podId } = this.props;
const requestedSlideNum = Number.parseInt(event.target.value, 10);
this.handleFTWSlideChange();
if (event) event.currentTarget.blur();
skipToSlide(requestedSlideNum, podId);
}
@ -209,9 +237,10 @@ class PresentationToolbar extends PureComponent {
nextSlideHandler(event) {
const {
nextSlide, currentSlideNum, numberOfSlides, podId, endCurrentPoll,
nextSlide, currentSlideNum, numberOfSlides, podId, endCurrentPoll
} = this.props;
this.handleFTWSlideChange();
if (event) event.currentTarget.blur();
endCurrentPoll();
nextSlide(currentSlideNum, numberOfSlides, podId);
@ -219,9 +248,10 @@ class PresentationToolbar extends PureComponent {
previousSlideHandler(event) {
const {
previousSlide, currentSlideNum, podId, endCurrentPoll,
previousSlide, currentSlideNum, podId, endCurrentPoll
} = this.props;
this.handleFTWSlideChange();
if (event) event.currentTarget.blur();
endCurrentPoll();
previousSlide(currentSlideNum, podId);

View File

@ -29,7 +29,7 @@ const propTypes = {
fileSizeMax: PropTypes.number.isRequired,
filePagesMax: PropTypes.number.isRequired,
handleSave: PropTypes.func.isRequired,
dispatchTogglePresentationDownloadable: PropTypes.func.isRequired,
dispatchChangePresentationDownloadable: PropTypes.func.isRequired,
fileValidMimeTypes: PropTypes.arrayOf(PropTypes.shape).isRequired,
presentations: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
@ -362,7 +362,7 @@ class PresentationUploader extends Component {
this.deepMergeUpdateFileKey = this.deepMergeUpdateFileKey.bind(this);
this.updateFileKey = this.updateFileKey.bind(this);
this.getPresentationsToShow = this.getPresentationsToShow.bind(this);
this.handleToggleDownloadable = this.handleToggleDownloadable.bind(this);
this.handleDownloadableChange = this.handleDownloadableChange.bind(this);
}
componentDidUpdate(prevProps) {
@ -418,6 +418,16 @@ class PresentationUploader extends Component {
shouldUpdateState = true;
}
if (currentPropPres?.downloadableExtension !== prevPropPres?.downloadableExtension) {
presentation.downloadableExtension = currentPropPres.downloadableExtension;
shouldUpdateState = true;
}
if (currentPropPres?.filenameConverted !== prevPropPres?.filenameConverted) {
presentation.filenameConverted = currentPropPres.filenameConverted;
shouldUpdateState = true;
}
modPresentation.conversion = currentPropPres.conversion;
modPresentation.isRemovable = currentPropPres.isRemovable;
@ -599,7 +609,7 @@ class PresentationUploader extends Component {
handleSave,
selectedToBeNextCurrent,
presentations: propPresentations,
dispatchTogglePresentationDownloadable,
dispatchChangePresentationDownloadable,
} = this.props;
const { disableActions, presentations } = this.state;
const presentationsToSave = presentations;
@ -620,7 +630,7 @@ class PresentationUploader extends Component {
(p) => p.id === item.id && p.isDownloadable !== item.isDownloadable,
);
if (didDownloadableStateChange) {
dispatchTogglePresentationDownloadable(item, item.isDownloadable);
dispatchChangePresentationDownloadable(item, item.isDownloadable);
}
}
});
@ -660,12 +670,10 @@ class PresentationUploader extends Component {
return null;
}
handleToggleDownloadable(item) {
const { dispatchTogglePresentationDownloadable } = this.props;
handleDownloadableChange(item, fileStateType, downloadable) {
const { dispatchChangePresentationDownloadable } = this.props;
const oldDownloadableState = item.isDownloadable;
dispatchTogglePresentationDownloadable(item, !oldDownloadableState);
dispatchChangePresentationDownloadable(item, downloadable, fileStateType);
}
handleDismiss() {
@ -692,7 +700,7 @@ class PresentationUploader extends Component {
);
}
handleDownloadingOfPresentation(item, type) {
handleDownloadingOfPresentation(item, fileStateType) {
const {
exportPresentation,
intl,
@ -702,7 +710,7 @@ class PresentationUploader extends Component {
this.deepMergeUpdateFileKey(item.id, 'exportation', exportation);
if (exportation.status === EXPORT_STATUSES.EXPORTED && stopped) {
if (type === 'Original') {
if (fileStateType === 'Original' || fileStateType === 'Converted') {
if (!item.isDownloadable) {
notify(intl.formatMessage(intlMessages.downloadButtonAvailable, { 0: item.filename }), 'success');
}
@ -715,7 +723,7 @@ class PresentationUploader extends Component {
EXPORT_STATUSES.RUNNING,
EXPORT_STATUSES.COLLECTING,
EXPORT_STATUSES.PROCESSING,
].includes(exportation.status) && type !== 'Original') {
].includes(exportation.status) && fileStateType === 'Annotated') {
this.setState((prevState) => {
prevState.presExporting.add(item.id);
return {
@ -750,7 +758,7 @@ class PresentationUploader extends Component {
}
};
exportPresentation(item.id, observer, type);
exportPresentation(item.id, observer, fileStateType);
}
getPresentationsToShow() {
@ -1079,11 +1087,11 @@ class PresentationUploader extends Component {
isDownloadable={isDownloadable}
allowDownloadOriginal={allowDownloadOriginal}
allowDownloadWithAnnotations={allowDownloadWithAnnotations}
handleToggleDownloadable={this.handleToggleDownloadable}
handleDownloadableChange={this.handleDownloadableChange}
item={item}
closeModal={() => Session.set('showUploadPresentationView', false)}
handleDownloadingOfPresentation={(type) => this
.handleDownloadingOfPresentation(item, type)}
handleDownloadingOfPresentation={(fileStateType) => this
.handleDownloadingOfPresentation(item, fileStateType)}
/>
) : null}
{isRemovable ? (

View File

@ -36,7 +36,7 @@ export default withTracker(() => {
const {
dispatchDisableDownloadable,
dispatchEnableDownloadable,
dispatchTogglePresentationDownloadable,
dispatchChangePresentationDownloadable,
exportPresentation,
} = Service;
const isOpen = isPresentationEnabled() && (Session.get('showUploadPresentationView') || false);
@ -56,7 +56,7 @@ export default withTracker(() => {
renderPresentationItemStatus: PresUploaderToast.renderPresentationItemStatus,
dispatchDisableDownloadable,
dispatchEnableDownloadable,
dispatchTogglePresentationDownloadable,
dispatchChangePresentationDownloadable,
exportPresentation,
isOpen,
selectedToBeNextCurrent: Session.get('selectedToBeNextCurrent') || null,

View File

@ -38,22 +38,36 @@ const propTypes = {
formatMessage: PropTypes.func.isRequired,
}).isRequired,
handleDownloadingOfPresentation: PropTypes.func.isRequired,
handleToggleDownloadable: PropTypes.func.isRequired,
handleDownloadableChange: PropTypes.func.isRequired,
isDownloadable: PropTypes.bool.isRequired,
allowDownloadOriginal: PropTypes.bool.isRequired,
allowDownloadWithAnnotations: PropTypes.bool.isRequired,
item: PropTypes.shape({
id: PropTypes.string.isRequired,
filename: PropTypes.string.isRequired,
filenameConverted: PropTypes.string,
isCurrent: PropTypes.bool.isRequired,
temporaryPresentationId: PropTypes.string.isRequired,
temporaryPresentationId: PropTypes.string,
isDownloadable: PropTypes.bool.isRequired,
isRemovable: PropTypes.bool.isRequired,
conversion: PropTypes.shape,
upload: PropTypes.shape,
exportation: PropTypes.shape,
uploadTimestamp: PropTypes.number.isRequired,
conversion: PropTypes.shape({
done: PropTypes.bool,
error: PropTypes.bool,
status: PropTypes.string,
numPages: PropTypes.number,
pagesCompleted: PropTypes.number,
}),
upload: PropTypes.shape({
done: PropTypes.bool,
error: PropTypes.bool,
}).isRequired,
exportation: PropTypes.shape({
status: PropTypes.string,
}),
uploadTimestamp: PropTypes.string,
downloadableExtension: PropTypes.string,
}).isRequired,
closeModal: PropTypes.func.isRequired,
isRTL: PropTypes.bool.isRequired,
disabled: PropTypes.bool.isRequired,
};
@ -72,7 +86,7 @@ class PresentationDownloadDropdown extends PureComponent {
const {
intl,
handleDownloadingOfPresentation,
handleToggleDownloadable,
handleDownloadableChange,
isDownloadable,
allowDownloadOriginal,
allowDownloadWithAnnotations,
@ -82,30 +96,57 @@ class PresentationDownloadDropdown extends PureComponent {
this.menuItems = [];
const toggleDownloadOriginalPresentation = (enableDownload) => {
handleToggleDownloadable(item);
const { filenameConverted, filename, downloadableExtension } = item;
const convertedFileExtension = filenameConverted?.split('.').slice(-1)[0];
const originalFileExtension = filename?.split('.').slice(-1)[0];
const changeDownloadOriginalOrConvertedPresentation = (enableDownload, fileStateType) => {
handleDownloadableChange(item, fileStateType, enableDownload);
if (enableDownload) {
handleDownloadingOfPresentation('Original');
handleDownloadingOfPresentation(fileStateType);
}
closeModal();
};
if (allowDownloadOriginal) {
if (!isDownloadable) {
if (isDownloadable && !!downloadableExtension
&& downloadableExtension === originalFileExtension) {
this.menuItems.push({
key: this.actionsKey[0],
dataTest: 'enableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.enableOriginalPresentationDownload),
onClick: () => toggleDownloadOriginalPresentation(true),
dataTest: 'disableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.disableOriginalPresentationDownload,
{ 0: originalFileExtension }),
onClick: () => changeDownloadOriginalOrConvertedPresentation(false, 'Original'),
});
} else {
this.menuItems.push({
key: this.actionsKey[0],
dataTest: 'disableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.disableOriginalPresentationDownload),
onClick: () => toggleDownloadOriginalPresentation(false),
dataTest: 'enableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.enableOriginalPresentationDownload,
{ 0: originalFileExtension }),
onClick: () => changeDownloadOriginalOrConvertedPresentation(true, 'Original'),
});
}
if ((!!filenameConverted && filenameConverted !== '')
&& convertedFileExtension !== originalFileExtension) {
if (isDownloadable && !!downloadableExtension
&& downloadableExtension === convertedFileExtension) {
this.menuItems.push({
key: this.actionsKey[0],
dataTest: 'disableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.disableOriginalPresentationDownload,
{ 0: convertedFileExtension }),
onClick: () => changeDownloadOriginalOrConvertedPresentation(false, 'Converted'),
});
} else {
this.menuItems.push({
key: this.actionsKey[0],
dataTest: 'enableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.enableOriginalPresentationDownload,
{ 0: convertedFileExtension }),
onClick: () => changeDownloadOriginalOrConvertedPresentation(true, 'Converted'),
});
}
}
}
if (allowDownloadWithAnnotations) {
this.menuItems.push({
@ -123,7 +164,7 @@ class PresentationDownloadDropdown extends PureComponent {
}
render() {
const { intl, isRTL, disabled } = this.props;
const { intl, disabled } = this.props;
const customStyles = { zIndex: 9999 };
@ -147,8 +188,8 @@ class PresentationDownloadDropdown extends PureComponent {
elevation: 2,
getcontentanchorel: null,
fullwidth: 'true',
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
transformOrigin: { vertical: 'top', horizontal: 'left' },
}}
actions={this.getAvailableActions()}
/>

View File

@ -3,36 +3,11 @@ import PropTypes from 'prop-types';
import Styled from './styles';
const propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
children: PropTypes.shape.isRequired,
children: PropTypes.shape({}).isRequired,
disabled: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
onDoubleClick: PropTypes.func.isRequired,
onMouseDown: PropTypes.func.isRequired,
onMouseUp: PropTypes.func.isRequired,
onKeyPress: PropTypes.func.isRequired,
onKeyDown: PropTypes.func.isRequired,
onKeyUp: PropTypes.func.isRequired,
};
class PresentationDownloadDropdownWrapper extends PureComponent {
constructor(props) {
super(props);
// Bind Mouse Event Handlers
this.internalClickHandler = this.internalClickHandler.bind(this);
this.internalDoubleClickHandler = this.internalDoubleClickHandler.bind(this);
this.internalMouseDownHandler = this.internalMouseDownHandler.bind(this);
this.internalMouseUpHandler = this.internalMouseUpHandler.bind(this);
// Bind Keyboard Event Handlers
this.internalKeyPressHandler = this.internalKeyPressHandler.bind(this);
this.internalKeyDownHandler = this.internalKeyDownHandler.bind(this);
this.internalKeyUpHandler = this.internalKeyUpHandler.bind(this);
}
validateDisabled(eventHandler, ...args) {
const { disabled } = this.props;
if (!disabled && typeof eventHandler === 'function') {
@ -42,43 +17,6 @@ class PresentationDownloadDropdownWrapper extends PureComponent {
return null;
}
// Define Mouse Event Handlers
internalClickHandler(...args) {
const { onClick } = this.props;
return this.validateDisabled(onClick, ...args);
}
internalDoubleClickHandler(...args) {
const { onDoubleClick } = this.props;
return this.validateDisabled(onDoubleClick, ...args);
}
internalMouseDownHandler(...args) {
const { onMouseDown } = this.props;
return this.validateDisabled(onMouseDown, ...args);
}
internalMouseUpHandler(...args) {
const { onMouseUp } = this.props;
return this.validateDisabled(onMouseUp, ...args);
}
// Define Keyboard Event Handlers
internalKeyPressHandler(...args) {
const { onKeyPress } = this.props;
return this.validateDisabled(onKeyPress, ...args);
}
internalKeyDownHandler(...args) {
const { onKeyDown } = this.props;
return this.validateDisabled(onKeyDown, ...args);
}
internalKeyUpHandler(...args) {
const { onKeyUp } = this.props;
return this.validateDisabled(onKeyUp, ...args);
}
render() {
const {
disabled,
@ -89,15 +27,6 @@ class PresentationDownloadDropdownWrapper extends PureComponent {
<Styled.DropdownMenuWrapper
disabled={disabled}
aria-disabled={disabled}
// Render Mouse event handlers
onClick={this.internalClickHandler}
onDoubleClick={this.internalDoubleClickHandler}
onMouseDown={this.internalMouseDownHandler}
onMouseUp={this.internalMouseUpHandler}
// Render Keyboard event handlers
onKeyPress={this.internalKeyPressHandler}
onKeyDown={this.internalKeyDownHandler}
onKeyUp={this.internalKeyUpHandler}
>
{children}
</Styled.DropdownMenuWrapper>

View File

@ -56,6 +56,8 @@ const getPresentations = () => Presentations
id,
name,
exportation,
filenameConverted,
downloadableExtension,
} = presentation;
const uploadTimestamp = id.split('-').pop();
@ -72,11 +74,13 @@ const getPresentations = () => Presentations
conversion: conversion || { done: true, error: false },
uploadTimestamp,
exportation: exportation || { error: false },
filenameConverted,
downloadableExtension,
};
});
const dispatchTogglePresentationDownloadable = (presentation, newState) => {
makeCall('setPresentationDownloadable', presentation.id, newState);
const dispatchChangePresentationDownloadable = (presentation, newState, fileStateType) => {
makeCall('setPresentationDownloadable', presentation.id, newState, fileStateType);
};
const observePresentationConversion = (
@ -378,7 +382,7 @@ const getExternalUploadData = () => {
};
};
const exportPresentation = (presentationId, observer, type) => {
const exportPresentation = (presentationId, observer, fileStateType) => {
let lastStatus = {};
Tracker.autorun((c) => {
@ -407,7 +411,7 @@ const exportPresentation = (presentationId, observer, type) => {
});
});
makeCall('exportPresentation', presentationId, type);
makeCall('exportPresentation', presentationId, fileStateType);
};
function handleFiledrop(files, files2, that, intl, intlMessages) {
@ -483,7 +487,7 @@ export default {
handleSavePresentation,
getPresentations,
persistPresentationChanges,
dispatchTogglePresentationDownloadable,
dispatchChangePresentationDownloadable,
setPresentation,
requestPresentationUploadToken,
getExternalUploadData,

View File

@ -80,14 +80,35 @@ const _trackStreamTermination = (stream, handler) => {
if (typeof stream !== 'object' || typeof handler !== 'function') {
throw new TypeError('Invalid trackStreamTermination arguments');
}
let _handler = handler;
if (stream.oninactive === null) {
// Dirty, but effective way of checking whether the browser supports the 'inactive'
// event. If the oninactive interface is null, it can be overridden === supported.
// If undefined, it's not; so we fallback to the track 'ended' event.
// The track ended listener should probably be reviewed once we create
// thin wrapper classes for MediaStreamTracks as well, because we'll want a single
// media stream holding multiple tracks in the future
if (stream.oninactive !== undefined) {
if (typeof stream.oninactive === 'function') {
const oldHandler = stream.oninactive;
_handler = () => {
oldHandler();
handler();
};
}
stream.addEventListener('inactive', handler, { once: true });
} else {
const track = MediaStreamUtils.getVideoTracks(stream)[0];
if (track) {
track.addEventListener('ended', handler, { once: true });
track.onended = handler;
if (typeof track.onended === 'function') {
const oldHandler = track.onended;
_handler = () => {
oldHandler();
handler();
};
}
track.onended = _handler;
}
}
};

View File

@ -73,14 +73,14 @@ const UserActions = (props) => {
const userId = user?.userId;
const isPinnedIntlKey = !pinned ? 'pin' : 'unpin';
const isFocusedIntlKey = !focused ? 'focus' : 'unfocus';
const enableSelfCamIntlKey = !isSelfViewDisabled ? 'disable' : 'enable';
const disabledCams = Session.get('disabledCams') || [];
const isCameraDisabled = disabledCams && disabledCams?.includes(cameraId);
const enableSelfCamIntlKey = !isCameraDisabled ? 'disable' : 'enable';
const menuItems = [];
const toggleDisableCam = () => {
const disabledCams = Session.get('disabledCams') || [];
const isDisabled = disabledCams && disabledCams?.includes(cameraId);
if (!isDisabled) {
if (!isCameraDisabled) {
Session.set('disabledCams', [...disabledCams, cameraId]);
} else {
Session.set('disabledCams', disabledCams.filter((cId) => cId !== cameraId));

View File

@ -89,8 +89,8 @@ class WakeLock extends Component {
if (result && result.error) {
Settings.application.wakeLock = false;
Settings.save();
this.feedbackToast(result);
}
this.feedbackToast(result);
});
}

View File

@ -448,9 +448,10 @@ export default function Whiteboard(props) {
}, [tldrawAPI?.getPageState()?.camera, presentationWidth, presentationHeight]);
React.useEffect(() => {
if (isPresenter && slidePosition) {
const currentZoom = calculateZoom(slidePosition?.viewBoxWidth, slidePosition?.viewBoxHeight);
tldrawAPI?.setCamera([slidePosition?.x, slidePosition?.y], currentZoom);
if (isPresenter && slidePosition && tldrawAPI) {
const camera = tldrawAPI?.getPageState()?.camera;
const newZoom = calculateZoom(slidePosition?.viewBoxWidth, slidePosition?.viewBoxHeight);
tldrawAPI?.setCamera([camera?.point[0], camera?.point[1]], newZoom);
}
}, [slidePosition?.viewBoxWidth, slidePosition?.viewBoxHeight]);
@ -762,7 +763,7 @@ export default function Whiteboard(props) {
if (camera.point[0] === 0 && camera.point[1] === 0) {
const newZoom = calculateZoom(slidePosition.viewBoxWidth, slidePosition.viewBoxHeight);
e?.setCamera([slidePosition.x, slidePosition.y], newZoom);
e?.setCamera([camera.point[0], camera.point[1]], newZoom);
}
const zoomFitSlide = calculateZoom(slidePosition.width, slidePosition.height);

View File

@ -385,8 +385,9 @@ const notifyShapeNumberExceeded = (intl, limit) => {
};
const toggleToolsAnimations = (activeAnim, anim, time) => {
const tdTools = document.querySelector("#TD-Tools");
const topToolbar = document.getElementById("TD-Styles")?.parentElement;
const tdTools = document.querySelector('#TD-Tools');
const topToolbar = document.getElementById('TD-Styles')?.parentElement;
const optionsDropdown = document.getElementById('WhiteboardOptionButton');
if (tdTools && topToolbar) {
tdTools.classList.remove(activeAnim);
topToolbar.classList.remove(activeAnim);
@ -395,6 +396,11 @@ const toggleToolsAnimations = (activeAnim, anim, time) => {
tdTools?.classList?.add(anim);
topToolbar?.classList?.add(anim);
}
if (optionsDropdown) {
optionsDropdown.classList.remove(activeAnim);
optionsDropdown.style.transition = `opacity ${time} ease-in-out`;
optionsDropdown?.classList?.add(anim);
}
}
export {

View File

@ -1,35 +1,41 @@
export function throttle(func, delay, options = {}) {
let lastInvocation = 0;
let isWaiting = false;
let timeoutId;
let lastExecTime = 0;
let leadingExec = true;
const leading = options.leading !== undefined ? options.leading : true;
const trailing = options.trailing !== undefined ? options.trailing : true;
const { leading = true, trailing = true } = options;
return function throttled(...args) {
const invokeFunction = () => {
lastInvocation = Date.now();
isWaiting = false;
func.apply(this, args);
};
let cancelPendingExecution = false; // Flag to track cancellation
if (!isWaiting) {
if (leading) {
invokeFunction();
} else {
isWaiting = true;
}
const throttledFunction = function () {
const context = this;
const args = arguments;
const elapsed = Date.now() - lastExecTime;
const currentTime = Date.now();
const timeSinceLastInvocation = currentTime - lastInvocation;
if (timeSinceLastInvocation >= delay) {
clearTimeout(timeoutId);
invokeFunction();
} else if (trailing) {
clearTimeout(timeoutId);
timeoutId = setTimeout(invokeFunction, delay - timeSinceLastInvocation);
function execute() {
if (!cancelPendingExecution) { // Only execute if not cancelled
func.apply(context, args);
lastExecTime = Date.now();
}
}
if (leadingExec && leading) {
execute();
leadingExec = false;
} else if (!timeoutId && trailing) {
timeoutId = setTimeout(function () {
execute();
timeoutId = null;
}, delay - elapsed);
}
};
}
// Add a cancel method to the throttled function
throttledFunction.cancel = function () {
cancelPendingExecution = true;
clearTimeout(timeoutId);
timeoutId = null;
};
return throttledFunction;
}

View File

@ -2,10 +2,12 @@
"app.home.greeting": "La presentació començarà en breu...",
"app.chat.submitLabel": "Envia el missatge",
"app.chat.loading": "Missatges del xat carregats: {0}%",
"app.chat.errorMaxMessageLength": "El missatge és {0} caràcter(s) massa llarg",
"app.chat.errorMaxMessageLength": "El missatge és {0} caràcters massa llarg",
"app.chat.disconnected": "Esteu fora de línia, els missatges no es poden enviar",
"app.chat.locked": "El xat està bloquejat, els missatges no es poden enviar",
"app.chat.inputLabel": "Entrada del missatge pel xat {0}",
"app.chat.emojiButtonLabel": "Selector Emoji",
"app.chat.loadMoreButtonLabel": "Més informació",
"app.chat.inputPlaceholder": "Missatge {0}",
"app.chat.titlePublic": "Xat públic",
"app.chat.titlePrivate": "Xat privat amb {0}",
@ -21,13 +23,56 @@
"app.chat.offline": "Fora de línia",
"app.chat.pollResult": "Resultats de l'enquesta",
"app.chat.breakoutDurationUpdated": "El temps de descans és ara {0} minuts",
"app.chat.breakoutDurationUpdatedModerator": "El temps de les sales de descans és ara de {0} minuts, i s'ha enviat una notificació.",
"app.chat.emptyLogLabel": "Registre del xat buit",
"app.chat.away": "Està fora",
"app.chat.notAway": "Ja no està fora",
"app.chat.clearPublicChatMessage": "L'historial del xat públic ha estat esborrat pel moderador",
"app.chat.multi.typing": "Diversos usuaris estan escrivint",
"app.chat.someone.typing": "Algú està escrivint",
"app.chat.one.typing": "{0} està escrivint",
"app.chat.two.typing": "{0} i {1} estan escrivint",
"app.chat.copySuccess": "Còpia de la transcripció del xat",
"app.chat.copyErr": "Error en la còpia de la transcripció del xat",
"app.emojiPicker.search": "Buscar",
"app.emojiPicker.notFound": "No s'ha trobat cap emoji",
"app.emojiPicker.skintext": "Tria el teu to de pell per defecte",
"app.emojiPicker.clear": "Clar",
"app.emojiPicker.categories.label": "Categories de emoji",
"app.emojiPicker.categories.people": "Persones i cos",
"app.emojiPicker.categories.reactions": "Reaccions",
"app.emojiPicker.categories.nature": "Animals i naturalesa",
"app.emojiPicker.categories.foods": "Menjar i beguda",
"app.emojiPicker.categories.places": "Viatges i llocs",
"app.emojiPicker.categories.activity": "Activitat",
"app.emojiPicker.categories.objects": "Objectes",
"app.emojiPicker.categories.symbols": "Símbols",
"app.emojiPicker.categories.flags": "Banderes",
"app.emojiPicker.categories.recent": "Utilitzat amb freqüència",
"app.emojiPicker.categories.search": "Resultats de cerca",
"app.emojiPicker.skintones.1": "To de pell per defecte",
"app.emojiPicker.skintones.2": "To de pell clar",
"app.emojiPicker.skintones.3": "To de pell mig - clar",
"app.emojiPicker.skintones.4": "To de pell mig",
"app.emojiPicker.skintones.5": "To de pell mig-fosc",
"app.emojiPicker.skintones.6": "To de pell fosc",
"app.timer.title": "Temps",
"app.timer.stopwatch.title": "Cronòmetre",
"app.timer.timer.title": "Temporitzador",
"app.timer.hideTimerLabel": "Ocultar temps",
"app.timer.button.stopwatch": "Cronòmetre",
"app.timer.button.timer": "Temporitzador",
"app.timer.button.start": "Inicia",
"app.timer.button.stop": "Aturar",
"app.timer.button.reset": "Reiniciar",
"app.timer.hours": "Hores",
"app.timer.minutes": "minuts",
"app.timer.seconds": "segons",
"app.timer.songs": "Cançons",
"app.timer.noTrack": "Sense cançó",
"app.timer.track1": "Relaxant",
"app.timer.track2": "Calma",
"app.timer.track3": "Feliç",
"app.captions.label": "Captures",
"app.captions.menu.close": "Tanca",
"app.captions.menu.start": "Inicia",
@ -53,12 +98,23 @@
"app.captions.speech.start": "Comença el reconeixement de veu",
"app.captions.speech.stop": "El reconeixement de veu s'ha aturat",
"app.captions.speech.error": "El reconeixement de veu s'ha aturat a causa de la incompatibilitat del navegador o a un temps de silenci",
"app.confirmation.skipConfirm": "No tornis a preguntar",
"app.confirmation.virtualBackground.title": "Iniciar nou fons virtual",
"app.confirmation.virtualBackground.description": "{0} s'afegirà com a fons virtual. Continuar?",
"app.confirmationModal.yesLabel": "Sí",
"app.textInput.sendLabel": "Enviat",
"app.title.defaultViewLabel": "Vista de presentació per defecte",
"app.notes.title": "Notes compartides",
"app.notes.titlePinned": "Notes compartides (Fixat)",
"app.notes.pinnedNotification": "Les Notes compartides estan ara fixades a la pissarra.",
"app.notes.label": "Notes",
"app.notes.hide": "Amagar notes",
"app.notes.locked": "Bloquejat/da",
"app.notes.disabled": "Fixat en l'àrea multimèdia",
"app.notes.notesDropdown.covertAndUpload": "Convertir notes en presentació",
"app.notes.notesDropdown.pinNotes": "Fixar notes a la pissarra",
"app.notes.notesDropdown.unpinNotes": "No fixar notes",
"app.notes.notesDropdown.notesOptions": "Opcions de notes",
"app.pads.hint": "Premi Esc per a enfocar la barra d'eines de la tauleta",
"app.user.activityCheck": "Revisió de l'activitat d'usuari",
"app.user.activityCheck.label": "Comprova si l'usuari encara està a la reunió ({0})",
@ -68,6 +124,7 @@
"app.userList.messagesTitle": "Missatges",
"app.userList.notesTitle": "Notes",
"app.userList.notesListItem.unreadContent": "Nou contingut disponible a la secció de notes compartides",
"app.userList.timerTitle": "Temps",
"app.userList.captionsTitle": "Subtítols",
"app.userList.presenter": "Presentador/a",
"app.userList.you": "Vós",
@ -82,6 +139,8 @@
"app.userList.menuTitleContext": "Opcions disponibles",
"app.userList.chatListItem.unreadSingular": "Un nou missatge",
"app.userList.chatListItem.unreadPlural": "{0} nous missatges",
"app.userList.menu.away": "Posa't com a absent",
"app.userList.menu.notAway": "Posa't com a actiu",
"app.userList.menu.chat.label": "Inicia xat privat",
"app.userList.menu.clearStatus.label": "Neteja l'estat",
"app.userList.menu.removeUser.label": "Elimina usuari/ària",
@ -106,6 +165,8 @@
"app.userList.userOptions.muteAllDesc": "Silencia tots els usuaris de la reunió",
"app.userList.userOptions.clearAllLabel": "Neteja les icones d'estat",
"app.userList.userOptions.clearAllDesc": "Neteja les icones d'estat de tots els usuaris",
"app.userList.userOptions.clearAllReactionsLabel": "Esborrar totes les reaccions",
"app.userList.userOptions.clearAllReactionsDesc": "Esborra tots les reaccions emojis dels usuaris",
"app.userList.userOptions.muteAllExceptPresenterLabel": "Silencia tothom excepte el presentador/a",
"app.userList.userOptions.muteAllExceptPresenterDesc": "Silencia tothom de la reunió excepte el presentador/a",
"app.userList.userOptions.unmuteAllLabel": "Apaga el silenciador de la reunió",
@ -122,6 +183,7 @@
"app.userList.userOptions.hideUserList": "Llista d'usuaris amagada ara per als assistents",
"app.userList.userOptions.webcamsOnlyForModerator": "Només els moderadors veuen les webcams dels assistents (bloquejat en opcions)",
"app.userList.content.participants.options.clearedStatus": "Estat dels usuaris netejat",
"app.userList.content.participants.options.clearedReactions": "Reaccions dels usuaris netejat",
"app.userList.userOptions.enableCam": "Webcams dels assistents activades",
"app.userList.userOptions.enableMic": "Micròfons dels assistents activats",
"app.userList.userOptions.enablePrivChat": "Xat privat actiu",
@ -143,6 +205,11 @@
"app.media.screenshare.notSupported": "L'ús compartit de la pantalla no és compatible amb aquest navegador.",
"app.media.screenshare.autoplayBlockedDesc": "Ens cal el vostre permís per a mostrar-vos la pantalla del presentador",
"app.media.screenshare.autoplayAllowLabel": "Mostra la pantalla compartida",
"app.media.cameraAsContent.start": "La càmera actual s'ha encès",
"app.media.cameraAsContent.end": "La càmera actual s'ha apagat",
"app.media.cameraAsContent.endDueToDataSaving": "Cambra actual parada per estalvi de dades",
"app.media.cameraAsContent.autoplayBlockedDesc": "Necessitem el teu permís per a mostrar-te la càmera del presentador.",
"app.media.cameraAsContent.autoplayAllowLabel": "Veure càmera actual",
"app.screenshare.presenterLoadingLabel": "La seva pantalla s'està carregant",
"app.screenshare.viewerLoadingLabel": "La pantalla del presentador s'està carregant",
"app.screenshare.presenterSharingLabel": "Ara està compartint la seva pantalla",
@ -151,6 +218,9 @@
"app.screenshare.screenshareRetryOtherEnvError": "Codi {0}. No s'ha pogut compartir la pantalla. Torna a intentar-ho amb un altre navegador o dispositiu.",
"app.screenshare.screenshareUnsupportedEnv": "Codi {0}. El navegador no és compatible. Torna a intentar-lo amb un altre navegador o dispositiu.",
"app.screenshare.screensharePermissionError": "Codi {0}. És necessari concedir permís per a capturar la pantalla.",
"app.cameraAsContent.presenterLoadingLabel": "La teva càmera s'està carregant",
"app.cameraAsContent.viewerLoadingLabel": "La càmera del presentador s'està carregant",
"app.cameraAsContent.presenterSharingLabel": "Ara estàs presentant la teva càmera",
"app.meeting.ended": "La sessió ha finalitzat",
"app.meeting.meetingTimeRemaining": "Temps restant de la reunió: {0}",
"app.meeting.meetingTimeHasEnded": "Temps finalitzat. La reunió es tancarà aviat",
@ -165,6 +235,7 @@
"app.presentation.hide": "Amaga la presentació",
"app.presentation.notificationLabel": "Presentació actual",
"app.presentation.downloadLabel": "Baixa",
"app.presentation.actionsLabel": "Accions",
"app.presentation.slideContent": "Contingut de la diapositiva",
"app.presentation.startSlideContent": "Inici del contingut de la diapositiva",
"app.presentation.endSlideContent": "Final del contingut de la diapositiva",
@ -201,9 +272,31 @@
"app.presentation.presentationToolbar.fitToWidth": "Ajusta a l'amplada",
"app.presentation.presentationToolbar.fitToPage": "Ajusta a la pàgina",
"app.presentation.presentationToolbar.goToSlide": "Diapositiva {0}",
"app.presentation.presentationToolbar.hideToolsDesc": "Ocultar barres d'eines",
"app.presentation.presentationToolbar.showToolsDesc": "Mostrar barres d'eines",
"app.presentation.placeholder": "No hi ha cap presentació activa actualment",
"app.presentationUploder.title": "Presentació",
"app.presentationUploder.message": "Com a presentador, podeu pujar qualsevol document o fitxer PDF. Recomanem el fitxer PDF per a obtenir millors resultats. Assegureu-vos que s'ha seleccionat una presentació mitjançant la casella circular de selecció que hi ha a la part dreta.",
"app.presentationUploader.exportHint": "En el menú \"Opcions d'exportació\" tens l'opció d'activar la descàrrega de la presentació original i proporcionar als usuaris un enllaç de descàrrega amb anotacions en el xat públic.",
"app.presentationUploader.exportToastHeader": "Enviar al xat ({0} element)",
"app.presentationUploader.exportToastHeaderPlural": "Enviar al xat ({0} elements)",
"app.presentationUploader.exporting": "Enviant al xat",
"app.presentationUploader.sending": "Enviant...",
"app.presentationUploader.collecting": "Extraient diapositiva {0} de {1}...",
"app.presentationUploader.processing": "Anotant diapositiva {0} de {1}...",
"app.presentationUploader.sent": "Enviat",
"app.presentationUploader.exportingTimeout": "L'exportació està trigant massa...",
"app.presentationUploader.export": "Enviar al xat",
"app.presentationUploader.exportCurrentStatePresentation": "Enviar un enllaç de descàrrega de la presentació amb anotacions en la pissarra",
"app.presentationUploader.enableOriginalPresentationDownload": "Activar la descàrrega de la presentació ({0})",
"app.presentationUploader.disableOriginalPresentationDownload": "Desactivar la descarga de la presentación original ({0})",
"app.presentationUploader.dropdownExportOptions": "Opcions d'exportació",
"app.presentationUploader.export.linkAvailable": "Enllaç de descàrrega {0} disponible en el xat públic.",
"app.presentationUploader.export.downloadButtonAvailable": "El botó de descàrrega de la presentació {0} està disponible.",
"app.presentationUploader.export.notAccessibleWarning": "pot no complir els requisits d'accessibilitat",
"app.presentationUploader.export.originalLabel": "Original",
"app.presentationUploader.export.inCurrentStateLabel": "En estat actual",
"app.presentationUploader.currentPresentationLabel": "Presentació actual",
"app.presentationUploder.extraHint": "IMPORTANT: cada arxiu no pot superar els {0} MB i les {1} pàgines.",
"app.presentationUploder.uploadLabel": "Carrega",
"app.presentationUploder.confirmLabel": "Confirma",
@ -214,11 +307,14 @@
"app.presentationUploder.dropzoneImagesLabel": "Arrossegueu aquí les imatges per a pujar-les",
"app.presentationUploder.browseFilesLabel": "o cerqueu en els vostres fitxers",
"app.presentationUploder.browseImagesLabel": "o cerqueu/captureu imatges",
"app.presentationUploder.externalUploadTitle": "Afegir contingut d'aplicacions de 3rs",
"app.presentationUploder.externalUploadLabel": "Examinar arxius",
"app.presentationUploder.fileToUpload": "Per a pujar ...",
"app.presentationUploder.currentBadge": "Actual",
"app.presentationUploder.rejectedError": "S'han rebutjat els fitxers següents. Comproveu-ne el tipus de fitxer.",
"app.presentationUploder.connectionClosedError": "Interromput per mala connexió. Si us plau, intenti-ho de nou.",
"app.presentationUploder.upload.progress": "S'està pujant ({0}&)",
"app.presentationUploder.conversion.204": "No hi ha contingut que recopilar",
"app.presentationUploder.upload.413": "L'arxiu és massa gran, ha superat el màxim de {0} MB",
"app.presentationUploder.genericError": "Ui! Alguna cosa ha anat malament...",
"app.presentationUploder.upload.408": "El temps de la sol·licitud de càrrega del token s'ha acabat",
@ -230,14 +326,14 @@
"app.presentationUploder.conversion.generatedSlides": "Diapositives generades ...",
"app.presentationUploder.conversion.generatingSvg": "S'estan generant les imatges SVG ...",
"app.presentationUploder.conversion.pageCountExceeded": "El nombre de pàgines ha superat el màxim de {0}",
"app.presentationUploder.conversion.invalidMimeType": "Format no vàlid detectat (extensió={0}, tipus de contingut={1})",
"app.presentationUploder.conversion.conversionTimeout": "La diapositiva {0} no ha pogut ser processada en {1} intents.",
"app.presentationUploder.conversion.officeDocConversionInvalid": "S'ha produït un error en processar el document. Si us plau, pugeu un PDF en el seu lloc.",
"app.presentationUploder.conversion.officeDocConversionFailed": "S'ha produït un error en processar el document. Si us plau, pugeu un PDF en el seu lloc.",
"app.presentationUploder.conversion.pdfHasBigPage": "No hemos podido convertir el archivo PDF, por favor, intente optimizarlo. Tamaño máximo de página {0}",
"app.presentationUploder.conversion.timeout": "Oh oh, la conversió ha trigat massa",
"app.presentationUploder.conversion.pageCountFailed": "S'ha produït un error en determinar el nombre de pàgines",
"app.presentationUploder.conversion.unsupportedDocument": "Extensió d'arxiu no admesa",
"app.presentationUploder.isDownloadableLabel": "No és permès de baixar la presentació. Feu clic per a permetre baixar la presentació",
"app.presentationUploder.isNotDownloadableLabel": "Es permet baixar la presentació - feu clic per impedir que la presentació es pugui baixar",
"app.presentationUploder.removePresentationLabel": "Elimina la presentació",
"app.presentationUploder.setAsCurrentPresentation": "Configura la presentació com a actual",
"app.presentationUploder.tableHeading.filename": "Nom del fitxer",
@ -251,6 +347,10 @@
"app.presentationUploder.clearErrors": "Neteja els errors",
"app.presentationUploder.clearErrorsDesc": "Neteja els intents fallats de pujada de presentació",
"app.presentationUploder.uploadViewTitle": "Pujar presentació",
"app.poll.questionAndoptions.label" : "Text de la pregunta a mostrar.\nA. Opció d'enquesta *\nB. Opció d'enquesta (opcional)\nC. Opció d'enquesta (opcional)\nD. Opció d'enquesta (opcional)\nE. Opció d'enquesta (opcional)",
"app.poll.customInput.label": "Entrada personalitzada",
"app.poll.customInputInstructions.label": "L'entrada personalitzada està habilitada - escriu la pregunta de l'enquesta i la opció(ns) en el format donat o arrossega i deixa un arxiu de text en el mateix format.",
"app.poll.maxOptionsWarning.label": "Només es poden utilitzar les 5 primeres opcions.",
"app.poll.pollPaneTitle": "Enquesta",
"app.poll.enableMultipleResponseLabel": "Permetre múltiples respostes per enquestat?",
"app.poll.quickPollTitle": "Enquesta ràpida",
@ -342,6 +442,7 @@
"app.navBar.settingsDropdown.hotkeysLabel": "Dreceres de teclat",
"app.navBar.settingsDropdown.hotkeysDesc": "Llista de les dreceres de teclat disponible",
"app.navBar.settingsDropdown.helpLabel": "Ajuda",
"app.navBar.settingsDropdown.openAppLabel": "Obrir a l'aplicació BigBlueButton Tauleta",
"app.navBar.settingsDropdown.helpDesc": "Enllaça l'usuari amb els videotutorials (obre una pestanya nova)",
"app.navBar.settingsDropdown.endMeetingDesc": "Finalitza la reunió actual",
"app.navBar.settingsDropdown.endMeetingLabel": "Finalitza la reunió",
@ -369,6 +470,15 @@
"app.about.confirmDesc": "D'acord",
"app.about.dismissLabel": "Cancel·la",
"app.about.dismissDesc": "Tanca la informació sobre el client",
"app.mobileAppModal.title": "Obrir l'aplicació BigBlueButton Tauleta",
"app.mobileAppModal.description": "Tens instal·lada l'aplicació BigBlueButton Tauleta en el teu dispositiu?",
"app.mobileAppModal.openApp": "Sí, obre l'aplicació ara",
"app.mobileAppModal.obtainUrlMsg": "Obtenir la URL de la reunió",
"app.mobileAppModal.obtainUrlErrorMsg": "Error en intentar obtenir la URL de la reunió",
"app.mobileAppModal.openStore": "No, obre l'App Store per a descarregar",
"app.mobileAppModal.dismissLabel": "Cancel·lar",
"app.mobileAppModal.dismissDesc": "Tanca",
"app.mobileAppModal.userConnectedWithSameId": "L'usuari {0} acaba de connectar-se utilitzant el mateix ID que tu.",
"app.actionsBar.changeStatusLabel": "Canvia estat",
"app.actionsBar.muteLabel": "Silencia",
"app.actionsBar.unmuteLabel": "Activa el micròfon",
@ -379,10 +489,16 @@
"app.actionsBar.actionsDropdown.restorePresentationDesc": "Botó per a restaurar la presentació després d'haver-la minimitzat",
"app.actionsBar.actionsDropdown.minimizePresentationLabel": "Minimitzar la presentació",
"app.actionsBar.actionsDropdown.minimizePresentationDesc": "Botó utilitzat per a minimitzar la presentació",
"app.actionsBar.actionsDropdown.layoutModal": "Gestionar el disseny",
"app.actionsBar.actionsDropdown.shareCameraAsContent": "Compartir la cambra com a contingut",
"app.actionsBar.actionsDropdown.unshareCameraAsContent": "Aturar la cambra com a contingut",
"app.screenshare.screenShareLabel" : "Comparteix pantalla",
"app.cameraAsContent.cameraAsContentLabel" : "Càmera actual",
"app.submenu.application.applicationSectionTitle": "Aplicació",
"app.submenu.application.animationsLabel": "Animació",
"app.submenu.application.audioFilterLabel": "Filtres d'àudio per a micròfon",
"app.submenu.application.wbToolbarsAutoHideLabel": "Ocultar automàticament les barres d'eines de la pissarra",
"app.submenu.application.darkThemeLabel": "Dark mode",
"app.submenu.application.fontSizeControlLabel": "Mida de la lletra",
"app.submenu.application.increaseFontBtnLabel": "Fes la mida de la lletra més gran",
"app.submenu.application.decreaseFontBtnLabel": "Fes la mida de la lletra més petita",
@ -391,7 +507,71 @@
"app.submenu.application.languageOptionLabel": "Trieu una llengua",
"app.submenu.application.noLocaleOptionLabel": "No hi ha fitxers de localització actius",
"app.submenu.application.paginationEnabledLabel": "Paginació de vídeos",
"app.submenu.application.wakeLockEnabledLabel": "Bloqueig de despertador",
"app.submenu.application.layoutOptionLabel": "Tipus de disseny",
"app.submenu.application.pushLayoutLabel": "Disseny push",
"app.submenu.application.localeDropdown.af": "Afrikaans",
"app.submenu.application.localeDropdown.ar": "Àrab",
"app.submenu.application.localeDropdown.az": "Azerbaidjan",
"app.submenu.application.localeDropdown.bg-BG": "Búlgar",
"app.submenu.application.localeDropdown.bn": "Bengalí",
"app.submenu.application.localeDropdown.ca": "Català",
"app.submenu.application.localeDropdown.cs-CZ": "Txec",
"app.submenu.application.localeDropdown.da": "Danès",
"app.submenu.application.localeDropdown.de": "Alemany",
"app.submenu.application.localeDropdown.dv": "Dhivehi",
"app.submenu.application.localeDropdown.el-GR": "Grec (Grècia)",
"app.submenu.application.localeDropdown.en": "Anglès",
"app.submenu.application.localeDropdown.eo": "Esperanto",
"app.submenu.application.localeDropdown.es": "Espanyol",
"app.submenu.application.localeDropdown.es-419": "Espanyol (Amèrica Llatina)",
"app.submenu.application.localeDropdown.es-ES": "Espanyol (Espanya)",
"app.submenu.application.localeDropdown.es-MX": "Espanyol (Mèxic)",
"app.submenu.application.localeDropdown.et": "Estonià",
"app.submenu.application.localeDropdown.eu": "Eusquera",
"app.submenu.application.localeDropdown.fa-IR": "Persa",
"app.submenu.application.localeDropdown.fi": "Finès",
"app.submenu.application.localeDropdown.fr": "Francès",
"app.submenu.application.localeDropdown.gl": "Gallec",
"app.submenu.application.localeDropdown.he": "Hebreu",
"app.submenu.application.localeDropdown.hi-IN": "Hindi",
"app.submenu.application.localeDropdown.hr": "Croat",
"app.submenu.application.localeDropdown.hu-HU": "Hongarès",
"app.submenu.application.localeDropdown.hy": "Armeni",
"app.submenu.application.localeDropdown.id": "indonesi",
"app.submenu.application.localeDropdown.it-IT": "Italià",
"app.submenu.application.localeDropdown.ja": "Japonès",
"app.submenu.application.localeDropdown.ka": "Georgià",
"app.submenu.application.localeDropdown.km": "Khmer",
"app.submenu.application.localeDropdown.kn": "Kannada",
"app.submenu.application.localeDropdown.ko-KR": "Coreà (Corea)",
"app.submenu.application.localeDropdown.lo-LA": "Lao",
"app.submenu.application.localeDropdown.lt-LT": "Lituà",
"app.submenu.application.localeDropdown.lv": "Letó",
"app.submenu.application.localeDropdown.ml": "Malayalam",
"app.submenu.application.localeDropdown.mn-MN": "Mongol",
"app.submenu.application.localeDropdown.nb-NO": "Noruec (bokmal)",
"app.submenu.application.localeDropdown.nl": "Holandès",
"app.submenu.application.localeDropdown.oc": "Occità",
"app.submenu.application.localeDropdown.pl-PL": "Polonès",
"app.submenu.application.localeDropdown.pt": "Portuguès",
"app.submenu.application.localeDropdown.pt-BR": "Portuguès (Brasil)",
"app.submenu.application.localeDropdown.ro-RO": "Romanès",
"app.submenu.application.localeDropdown.ru": "Rus",
"app.submenu.application.localeDropdown.sk-SK": "Eslovac (Eslovàquia)",
"app.submenu.application.localeDropdown.sl": "Eslovè",
"app.submenu.application.localeDropdown.sr": "Serbi",
"app.submenu.application.localeDropdown.sv-SE": "Suec",
"app.submenu.application.localeDropdown.ta": "Tamil",
"app.submenu.application.localeDropdown.te": "Telugu",
"app.submenu.application.localeDropdown.th": "Thai",
"app.submenu.application.localeDropdown.tr": "Turc",
"app.submenu.application.localeDropdown.tr-TR": "Turc (Turquia)",
"app.submenu.application.localeDropdown.uk-UA": "Ucraïnès",
"app.submenu.application.localeDropdown.vi": "Vietnamita",
"app.submenu.application.localeDropdown.vi-VN": "Vietnamita (Vietnam)",
"app.submenu.application.localeDropdown.zh-CN": "Xinès simplificat (la Xina)",
"app.submenu.application.localeDropdown.zh-TW": "Xinès tradicional (Taiwan)",
"app.submenu.notification.SectionTitle": "Notificacions",
"app.submenu.notification.Desc": "Definiu quines notificacions voleu rebre i com ho voleu fer.",
"app.submenu.notification.audioAlertLabel": "Alertes d'àudio",
@ -437,10 +617,11 @@
"app.talkingIndicator.moreThanMaxIndicatorsWereTalking" : "{0}+ estaven parlant",
"app.talkingIndicator.wasTalking" : "{0} ha parat de parlar",
"app.actionsBar.actionsDropdown.actionsLabel": "Accions",
"app.actionsBar.actionsDropdown.activateTimerStopwatchLabel": "Activar el temporitzador/cronòmetre",
"app.actionsBar.actionsDropdown.deactivateTimerStopwatchLabel": "Desactivar el temporitzador/cronòmetre",
"app.actionsBar.actionsDropdown.presentationLabel": "Gestionar les presentacions",
"app.actionsBar.actionsDropdown.initPollLabel": "Inicia una enquesta",
"app.actionsBar.actionsDropdown.desktopShareLabel": "Comparteix la teva pantalla",
"app.actionsBar.actionsDropdown.lockedDesktopShareLabel": "Compartir pantalla bloquejat",
"app.actionsBar.actionsDropdown.stopDesktopShareLabel": "Deixa de compartir pantalla",
"app.actionsBar.actionsDropdown.presentationDesc": "Puja la teva presentació",
"app.actionsBar.actionsDropdown.initPollDesc": "Inicia una enquesta",
@ -457,6 +638,10 @@
"app.actionsBar.actionsDropdown.takePresenterDesc": "Assigna't com el nou/nova presentador",
"app.actionsBar.actionsDropdown.selectRandUserLabel": "Seleccionar un usuari a l'atzar",
"app.actionsBar.actionsDropdown.selectRandUserDesc": "Tria un usuari d'entre els assistents disponibles de manera aleatòria",
"app.actionsBar.reactions.reactionsButtonLabel": "Barra de reaccions",
"app.actionsBar.reactions.raiseHand": "Aixecar la mà",
"app.actionsBar.reactions.lowHand": "Baixar la mà",
"app.actionsBar.reactions.autoCloseReactionsBarLabel": "Tancar automàticament la barra de reaccions",
"app.actionsBar.emojiMenu.statusTriggerLabel": "Defineix l'estat",
"app.actionsBar.emojiMenu.awayLabel": "He sortit",
"app.actionsBar.emojiMenu.awayDesc": "Canvia l'estat a 'he sortit'",
@ -496,6 +681,7 @@
"app.audioNotification.audioFailedError1012": "Connexió tancada (ICE error 1012)",
"app.audioNotification.audioFailedMessage": "La vostra connexió d'audio ha fallat",
"app.audioNotification.mediaFailedMessage": "getUserMicMedia ha fallat, ja que només es permeten els orígens segurs",
"app.audioNotification.deviceChangeFailed": "Error en el canvi de dispositiu d'àudio. Comprova si el dispositiu triat està correctament configurat i disponible.",
"app.audioNotification.closeLabel": "Tanca",
"app.audioNotificaion.reconnectingAsListenOnly": "El micròfon s'ha bloquejat per als espectadors; només esteu connectats com a audició",
"app.breakoutJoinConfirmation.title": "Unirse a la sala separada",
@ -509,6 +695,7 @@
"app.breakout.dropdown.manageDuration": "Canviar la durada",
"app.breakout.dropdown.destroyAll": "Finalitza les sales externes",
"app.breakout.dropdown.options": "Opcions de sales externes",
"app.breakout.dropdown.manageUsers": "Gestionar usuaris",
"app.calculatingBreakoutTimeRemaining": "Calculant temps restant ...",
"app.audioModal.ariaTitle": "Entra a l'àudio modal",
"app.audioModal.microphoneLabel": "Micròfon",
@ -555,6 +742,7 @@
"app.audio.changeAudioDevice": "Canviar el dispositiu d'àudio",
"app.audio.enterSessionLabel": "Entrar sessió",
"app.audio.playSoundLabel": "Reproduir el so",
"app.audio.stopAudioFeedback": "Detenir el feedback d'àudio",
"app.audio.backLabel": "Enrere",
"app.audio.loading": "Carregant",
"app.audio.microphones": "Micròfons",
@ -567,10 +755,32 @@
"app.audio.audioSettings.testSpeakerLabel": "Prova l'altaveu",
"app.audio.audioSettings.microphoneStreamLabel": "El seu volum d'emissió",
"app.audio.audioSettings.retryLabel": "Reintentar",
"app.audio.audioSettings.fallbackInputLabel": "Entrada d'àudio {0}",
"app.audio.audioSettings.fallbackOutputLabel": "Sortida d'àudio {0}",
"app.audio.audioSettings.defaultOutputDeviceLabel": "Per defecte",
"app.audio.audioSettings.findingDevicesLabel": "Trobar dispositius...",
"app.audio.listenOnly.backLabel": "Enrere",
"app.audio.listenOnly.closeLabel": "Tanca",
"app.audio.permissionsOverlay.title": "Permet l'accés al micròfon",
"app.audio.permissionsOverlay.hint": "Necessitem que ens permeteu utilitzar els vostres dispositius multimèdia per unir-vos a la conferència de veu :)",
"app.audio.captions.button.start": "Iniciar subtítols",
"app.audio.captions.button.stop": "Detenir subtítols",
"app.audio.captions.button.language": "Idioma",
"app.audio.captions.button.transcription": "Transcripció",
"app.audio.captions.button.transcriptionSettings": "Ajustos de transcripció",
"app.audio.captions.speech.title": "Transcripció automàtica",
"app.audio.captions.speech.disabled": "Desactivat",
"app.audio.captions.speech.unsupported": "El teu navegador no és compatible amb el reconeixement de veu. L'àudio no es transcriurà",
"app.audio.captions.select.de-DE": "Alemany",
"app.audio.captions.select.en-US": "Anglès",
"app.audio.captions.select.es-ES": "Espanyol",
"app.audio.captions.select.fr-FR": "Francès",
"app.audio.captions.select.hi-ID": "Hindi",
"app.audio.captions.select.it-IT": "Italià",
"app.audio.captions.select.ja-JP": "Japonès",
"app.audio.captions.select.pt-BR": "Portuguès",
"app.audio.captions.select.ru-RU": "Rus",
"app.audio.captions.select.zh-CN": "Xinès",
"app.error.removed": "Heu estat eliminat/da de la conferència",
"app.error.meeting.ended": "Us heu desconnectat de la conferència",
"app.meeting.logout.duplicateUserEjectReason": "Un usuari duplicat intenta unir-se a la reunió",
@ -578,6 +788,7 @@
"app.meeting.logout.ejectedFromMeeting": "Se us ha retirat de la reunió",
"app.meeting.logout.validateTokenFailedEjectReason": "No s'ha pogut validar el token d'autorització",
"app.meeting.logout.userInactivityEjectReason": "Usuari inactiu durant massa temps",
"app.meeting.logout.maxParticipantsReached": "S'ha aconseguit el nombre màxim de participants permès per a aquesta reunió",
"app.meeting-ended.rating.legendLabel": "Valoració de comentaris",
"app.meeting-ended.rating.starLabel": "Estrella",
"app.modal.close": "Tanca",
@ -599,8 +810,11 @@
"app.error.403": "Se us ha retirat de la reunió",
"app.error.404": "No trobat",
"app.error.408": "Error d'autenticació",
"app.error.409": "Conflicte",
"app.error.410": "La reunió ha finalitzat",
"app.error.500": "Oh oh, quelcom ha anat malament",
"app.error.503": "T'han desconnectat",
"app.error.disconnected.rejoin": "Pots actualitzar la pàgina per a tornar a entrar.",
"app.error.userLoggedOut": "L'usuari té un sessionToken no vàlid a causa del tancament de sessió",
"app.error.ejectedUser": "L'usuari té un sessionToken no vàlid a causa de l'expulsió",
"app.error.joinedAnotherWindow": "Aquesta sessió sembla estar oberta en una altra finestra del navegador.",
@ -609,7 +823,6 @@
"app.error.fallback.presentation.title": "Hi ha hagut un error",
"app.error.fallback.presentation.description": "S'ha registrat. Proveu de tornar a carregar la pàgina.",
"app.error.fallback.presentation.reloadButton": "Torna a carregar",
"app.guest.waiting": "En espera que s'aprovi l'entrada",
"app.guest.errorSeeConsole": "Error: més detalls en la consola.",
"app.guest.noModeratorResponse": "No hi ha resposta del Moderador.",
"app.guest.noSessionToken": "No es va rebre cap Token de sessió.",
@ -644,17 +857,27 @@
"app.userList.guest.privateMessageLabel": "Missatge",
"app.userList.guest.acceptLabel": "Accepta",
"app.userList.guest.denyLabel": "Denega",
"app.userList.guest.feedbackMessage": "Acció aplicada:",
"app.user-info.title": "Cerca al directori",
"app.toast.breakoutRoomEnded": "La sala separada ha tancat. Si us plau, torneu-vos a unir a l'àudio.",
"app.toast.chat.public": "Nou missatge del xat públic",
"app.toast.chat.private": "Nou missatge del xat privat",
"app.toast.chat.system": "Sistema",
"app.toast.chat.poll": "Resultats de l'enquesta",
"app.toast.chat.pollClick": "S'han publicat els resultats de l'enquesta. Prem aquí per a veure.",
"app.toast.clearedEmoji.label": "Estatus d'emoji net",
"app.toast.setEmoji.label": "Estatus d'emoji configurat {0}",
"app.toast.meetingMuteOn.label": "Tots els usuaris han estat silenciats",
"app.toast.meetingMuteOnViewers.label": "Tots els usuaris han estat silenciats",
"app.toast.meetingMuteOff.label": "Reunió silenciada desactivada",
"app.toast.wakeLock.acquireSuccess": "Bloqueig de despertador actiu. Pots desactivar-ho en el menú d'ajustos.",
"app.toast.wakeLock.acquireFailed": "Error en adquirir el bloqueig de despertador",
"app.toast.wakeLock.notSupported": "No s'admet el bloqueig del despertador",
"app.toast.wakeLock.disclaimer": "{0}. Sortirà de la trucada si la pantalla s'apaga.",
"app.toast.setEmoji.raiseHand": "Has aixecat la mà",
"app.toast.setEmoji.lowerHand": "La seva mà ha estat baixada",
"app.toast.setEmoji.away": "Has configurat el teu estat com a absent",
"app.toast.setEmoji.notAway": "Has eliminat el teu estat d'absent",
"app.toast.promotedLabel": "Has estat ascendit a Moderador",
"app.toast.demotedLabel": "Has estat reclassificat com a espectador",
"app.notification.recordingStart": "La sessió ara s'està enregistrant",
@ -667,6 +890,7 @@
"app.shortcut-help.title": "Dreceres de teclat",
"app.shortcut-help.accessKeyNotAvailable": "Claus d'accés no disponibles",
"app.shortcut-help.comboLabel": "Combo",
"app.shortcut-help.alternativeLabel": "Alternativa",
"app.shortcut-help.functionLabel": "Funció",
"app.shortcut-help.closeLabel": "Tanca",
"app.shortcut-help.closeDesc": "Tanca la modalitat de dreceres del teclat",
@ -688,6 +912,38 @@
"app.shortcut-help.toggleFullscreenKey": "Enter",
"app.shortcut-help.nextSlideKey": "Fletxa dreta",
"app.shortcut-help.previousSlideKey": "Fletxa esquerra",
"app.shortcut-help.select": "Seleccionar eina",
"app.shortcut-help.pencil": "Llapis",
"app.shortcut-help.eraser": "Goma",
"app.shortcut-help.rectangle": "Rectangle",
"app.shortcut-help.elipse": "El·lipse",
"app.shortcut-help.triangle": "Triangle",
"app.shortcut-help.line": "Línia",
"app.shortcut-help.arrow": "Fletxa",
"app.shortcut-help.text": "Eina de text",
"app.shortcut-help.note": "Nota adhesiva",
"app.shortcut-help.general": "General",
"app.shortcut-help.presentation": "Presentació",
"app.shortcut-help.whiteboard": "Pissarra",
"app.shortcut-help.zoomIn": "Apropa",
"app.shortcut-help.zoomOut": "Allunya",
"app.shortcut-help.zoomFit": "Restaura la mida",
"app.shortcut-help.zoomSelect": "Zoom a la selecció",
"app.shortcut-help.flipH": "Voltejar horitzontalment",
"app.shortcut-help.flipV": "Voltejar verticalment",
"app.shortcut-help.lock": "Bloquejar / Desbloquejar",
"app.shortcut-help.moveToFront": "Moure al capdavant",
"app.shortcut-help.moveToBack": "Moure al fons",
"app.shortcut-help.moveForward": "Avançar",
"app.shortcut-help.moveBackward": "Retrocedir",
"app.shortcut-help.undo": "Desfer",
"app.shortcut-help.redo": "Refer",
"app.shortcut-help.cut": "Tallar",
"app.shortcut-help.copy": "Copiar",
"app.shortcut-help.paste": "Enganxar",
"app.shortcut-help.selectAll": "Seleccionar tot",
"app.shortcut-help.delete": "Esborrar",
"app.shortcut-help.duplicate": "Duplicar",
"app.lock-viewers.title": "Bloqueja espectadors",
"app.lock-viewers.description": "Aquestes opcions permeten restringir els espectadors a l'ús de funcions específiques.",
"app.lock-viewers.featuresLable": "Característica",
@ -704,6 +960,7 @@
"app.lock-viewers.button.cancel": "Cancel·la",
"app.lock-viewers.locked": "Bloquejat/da",
"app.lock-viewers.hideViewersCursor": "Veure altres cursors dels espectadors",
"app.lock-viewers.hideAnnotationsLabel": "Veure les anotacions d'altres espectadors",
"app.guest-policy.ariaTitle": "Modalitat de configuració de la política de convidats",
"app.guest-policy.title": "Política de convidats",
"app.guest-policy.description": "Canviar la configuració de la política de convidats a les reunions",
@ -711,6 +968,7 @@
"app.guest-policy.button.alwaysAccept": "Acceptar sempre",
"app.guest-policy.button.alwaysDeny": "Negar sempre",
"app.guest-policy.policyBtnDesc": "Estableix la política de convidats a les reunions",
"app.guest-policy.feedbackMessage": "La política de convidats és la següent:",
"app.connection-status.ariaTitle": "Finestra modal de l'estat de connexió",
"app.connection-status.title": "Estat de connexió",
"app.connection-status.description": "Mostra l'estat de connexió dels usuaris",
@ -724,6 +982,7 @@
"app.connection-status.no": "No",
"app.connection-status.notification": "S'ha detectat una pèrdua en la seva connexió",
"app.connection-status.offline": "fora de línia",
"app.connection-status.clientNotRespondingWarning": "El client no respon",
"app.connection-status.audioUploadRate": "Velocitat de càrrega d'àudio",
"app.connection-status.audioDownloadRate": "Velocitat de descàrrega d'àudio",
"app.connection-status.videoUploadRate": "Velocitat de càrrega de vídeo",
@ -744,6 +1003,12 @@
"app.recording.resumeTitle": "Reprén l'enregistrament",
"app.recording.startDescription": "Podeu tornar a seleccionar el botó d'enregistrament per aturar-lo.",
"app.recording.stopDescription": "Esteu segur que voleu aturar l'enregistrament? Podeu reprendre'l seleccionant el botó de nou.",
"app.recording.notify.title": "L'enregistrament ha començat",
"app.recording.notify.description": "Es disposarà d'un enregistrament basat en el contingut restant d'aquesta sessió",
"app.recording.notify.continue": "Continuar",
"app.recording.notify.leave": "Abandonar sessió",
"app.recording.notify.continueLabel" : "Acceptar enregistrament i continuar",
"app.recording.notify.leaveLabel" : "No acceptar l'enregistrament i abandonar la reunió",
"app.videoPreview.cameraLabel": "Càmera",
"app.videoPreview.profileLabel": "Qualitat",
"app.videoPreview.quality.low": "Baixa",
@ -760,13 +1025,21 @@
"app.videoPreview.webcamOptionLabel": "Escull webcam",
"app.videoPreview.webcamPreviewLabel": "Previsualització de webcam",
"app.videoPreview.webcamSettingsTitle": "Configuració de la webcam",
"app.videoPreview.webcamEffectsTitle": "Efectes visuals de la webcam",
"app.videoPreview.cameraAsContentSettingsTitle": "Càmera actual",
"app.videoPreview.webcamVirtualBackgroundLabel": "Configuració del fons virtual",
"app.videoPreview.webcamVirtualBackgroundDisabledLabel": "Aquest dispositiu no admet fons virtuals",
"app.videoPreview.webcamNotFoundLabel": "Webcam no trobada",
"app.videoPreview.profileNotFoundLabel": "Perfil de càmera no suportada",
"app.videoPreview.brightness": "Lluentor",
"app.videoPreview.wholeImageBrightnessLabel": "Imatge completa",
"app.videoPreview.wholeImageBrightnessDesc": "Aplica lluentor al flux i a la imatge de fons",
"app.videoPreview.sliderDesc": "Augmentar o disminuir els nivells de lluentor",
"app.video.joinVideo": "Comparteix webcam",
"app.video.connecting": "Comença la compartició de la càmera web...",
"app.video.leaveVideo": "Atura compartir webcam",
"app.video.videoSettings": "Configuració de vídeo",
"app.video.visualEffects": "Efectes visuals",
"app.video.advancedVideo": "Obrir la configuració avançada",
"app.video.iceCandidateError": "Error en afegir un candidat ICE",
"app.video.iceConnectionStateError": "Connexió fallida (ICE error 1107)",
@ -782,6 +1055,7 @@
"app.video.notReadableError": "No s'ha pogut obtenir el vídeo amb càmera web. Assegureu-vos que un altre programa no utilitzi la càmera web",
"app.video.timeoutError": "El navegador no ha respòs a temps.",
"app.video.genericError": "S'ha produït un error desconegut en el dispositiu ({0})",
"app.video.inactiveError": "La teva webcam s'ha detingut inesperadament. Si us plau, revisa els permisos del teu navegador",
"app.video.mediaTimedOutError": "La transmissió de la teva webcam s'ha interromput. Intenta compartir-la de nou",
"app.video.mediaFlowTimeout1020": "Els suports de mitjans no van poder arribar al servidor (error 1020)",
"app.video.suggestWebcamLock": "Voleu aplicar la configuració de bloqueig a les càmeres web dels espectadors?",
@ -804,8 +1078,17 @@
"app.video.virtualBackground.board": "Tauler",
"app.video.virtualBackground.coffeeshop": "Cafeteria",
"app.video.virtualBackground.background": "Antecedents",
"app.video.virtualBackground.backgroundWithIndex": "Fons {0}",
"app.video.virtualBackground.custom": "Carregar des de l'ordinador",
"app.video.virtualBackground.remove": "Eliminar la imatge afegida",
"app.video.virtualBackground.genericError": "No s'ha pogut aplicar l'efecte de càmara. Intenta-ho de nou.",
"app.video.virtualBackground.camBgAriaDesc": "Estableix el fons virtual de la càmera web en {0}",
"app.video.virtualBackground.maximumFileSizeExceeded": "S'ha superat la grandària màxima d'arxiu. ({0}MB)",
"app.video.virtualBackground.typeNotAllowed": "Tipus d'arxiu no permès.",
"app.video.virtualBackground.errorOnRead": "Alguna cosa ha anat malament en llegir l'arxiu.",
"app.video.virtualBackground.uploaded": "Carregat",
"app.video.virtualBackground.uploading": "Carregant...",
"app.video.virtualBackground.button.customDesc": "Afegeix una nova imatge de fons virtual",
"app.video.camCapReached": "No es poden compartir més càmeres",
"app.video.meetingCamCapReached": "La reunió ha arribat al límit de càmeres simultànies",
"app.video.dropZoneLabel": "Deixar caure aquí",
@ -826,6 +1109,8 @@
"app.whiteboard.annotations.poll": "S'han publicat els resultats de l'enquesta",
"app.whiteboard.annotations.pollResult": "Resultat de l'enquesta",
"app.whiteboard.annotations.noResponses": "Sense respostes",
"app.whiteboard.annotations.notAllowed": "No estàs autoritzat a fer aquest canvi",
"app.whiteboard.annotations.numberExceeded": "El nombre d'anotacions ha superat el límit ({0})",
"app.whiteboard.toolbar.tools": "Eines",
"app.whiteboard.toolbar.tools.hand": "Panell",
"app.whiteboard.toolbar.tools.pencil": "Llapis",
@ -852,6 +1137,7 @@
"app.whiteboard.toolbar.color.silver": "Platejat",
"app.whiteboard.toolbar.undo": "Desfés anotació",
"app.whiteboard.toolbar.clear": "Neteja les anotacions",
"app.whiteboard.toolbar.clearConfirmation": "Estàs segur que vols esborrar totes les anotacions?",
"app.whiteboard.toolbar.multiUserOn": "Activa la pissarra multiusuari",
"app.whiteboard.toolbar.multiUserOff": "Desactiva la pissarra multiusuari",
"app.whiteboard.toolbar.palmRejectionOn": "Activar el rebuig del palmell de la mà",
@ -869,15 +1155,19 @@
"app.videoDock.webcamFocusDesc": "Centra la càmera seleccionada",
"app.videoDock.webcamUnfocusLabel": "Descentra",
"app.videoDock.webcamUnfocusDesc": "Descentra la càmera seleccionada",
"app.videoDock.webcamDisableLabel": "Desactivar la vista pròpia",
"app.videoDock.webcamDisableLabelAllCams": "Desactivar la vista pròpia (totes les càmeres)",
"app.videoDock.webcamEnableLabel": "Activar la vista pròpia",
"app.videoDock.webcamDisableDesc": "Vista pròpia desactivada",
"app.videoDock.webcamPinLabel": "Fixar",
"app.videoDock.webcamPinDesc": "Fixar la webcam seleccionada",
"app.videoDock.webcamFullscreenLabel": "Càmera web a pantalla completa",
"app.videoDock.webcamSqueezedButtonLabel": "Opcions de la webcam",
"app.videoDock.webcamUnpinLabel": "Desconnectar",
"app.videoDock.webcamUnpinLabelDisabled": "Només els moderadors poden desconnectar als usuaris",
"app.videoDock.webcamUnpinDesc": "Desconnectar la webcam seleccionada",
"app.videoDock.autoplayBlockedDesc": "Necessitem el seu permís per a mostrar les càmeres dels altres usuaris.",
"app.videoDock.autoplayAllowLabel": "Veure càmeres",
"app.invitation.title": "Invitació de sala separada",
"app.invitation.confirm": "Convidar",
"app.createBreakoutRoom.title": "Sales externes",
"app.createBreakoutRoom.ariaTitle": "Amaga les sales externes",
"app.createBreakoutRoom.breakoutRoomLabel": "Sales externes {0}",
@ -909,6 +1199,10 @@
"app.createBreakoutRoom.addRoomTime": "Augmenta el temps de la sala separada a ",
"app.createBreakoutRoom.addParticipantLabel": "+ Afegeix participant",
"app.createBreakoutRoom.freeJoin": "Permet els usuaris la sala a la que es volen unir",
"app.createBreakoutRoom.manageRoomsLabel": "Gestionar sales",
"app.createBreakoutRoom.captureNotes": "Guardar notes compartides",
"app.createBreakoutRoom.captureSlides": "Guardar pissarra",
"app.createBreakoutRoom.sendInvitationToMods": "Enviar invitació als moderadors assignats",
"app.createBreakoutRoom.leastOneWarnBreakout": "Com a mínim heu d'incloure un usuari en una sala separada.",
"app.createBreakoutRoom.minimumDurationWarnBreakout": "La durada mínima d'una sala de grup és de {0} minuts.",
"app.createBreakoutRoom.modalDesc": "Truc: Podeu arrossegar i deixar anar el nom d'usuari per a assignar-lo a una sala separada.",
@ -921,6 +1215,14 @@
"app.createBreakoutRoom.setTimeCancel": "Cancel·lar",
"app.createBreakoutRoom.setTimeHigherThanMeetingTimeError": "La durada de les sales externes no pot superar el temps restant de la reunió.",
"app.createBreakoutRoom.roomNameInputDesc": "Actualitza el nom de la sala de grup",
"app.createBreakoutRoom.movedUserLabel": "Mogut {0} a l'habitació {1}",
"app.updateBreakoutRoom.modalDesc": "Per a actualitzar o convidar a un usuari, n'hi ha prou amb arrossegar-lo a la sala desitjada.",
"app.updateBreakoutRoom.cancelLabel": "Cancel·lar",
"app.updateBreakoutRoom.title": "Actualitza la sala de grup",
"app.updateBreakoutRoom.confirm": "Aplica",
"app.updateBreakoutRoom.userChangeRoomNotification": "Has estat mogut a l'habitació {0}.",
"app.smartMediaShare.externalVideo": "Vídeo(s) extern",
"app.update.resetRoom": "Restablir sala d'usuari",
"app.externalVideo.start": "Comparteix un nou vídeo",
"app.externalVideo.title": "Comparteix un vídeo extern",
"app.externalVideo.input": "URL de vídeo extern",
@ -935,7 +1237,6 @@
"app.externalVideo.subtitlesOff": "Encendre (si està disponible)",
"app.actionsBar.actionsDropdown.shareExternalVideo": "Comparteix un vídeo extern",
"app.actionsBar.actionsDropdown.stopShareExternalVideo": "Deixa de compartir els vídeos externs",
"app.iOSWarning.label": "Actualitzeu a iOS 12.2 o superior",
"app.legacy.unsupportedBrowser": "Sembla que utilitzeu un navegador que no és compatible. Si us plau, utilitzeu {0} o {1} per obtenir assistència completa.",
"app.legacy.upgradeBrowser": "Sembla que utilitzeu una versió anterior d'un navegador compatible. Actualitzeu el navegador per obtenir assistència completa.",
"app.legacy.criosBrowser": "A iOS, utilitzeu Safari per obtenir assistència completa.",
@ -946,6 +1247,14 @@
"app.debugWindow.form.enableAutoarrangeLayoutDescription": "(es desactivarà si arrossega o canvia la grandària de l'àrea de les càmeres web)",
"app.debugWindow.form.chatLoggerLabel": "Provar els nivells del registre del xat",
"app.debugWindow.form.button.apply": "Aplicar",
"app.layout.modal.title": "Dissenys",
"app.layout.modal.update": "Actualització",
"app.layout.modal.updateAll": "Actualitzar a tothom",
"app.layout.modal.layoutLabel": "Selecciona el teu disseny",
"app.layout.modal.pushLayoutLabel": "Empènyer a tots",
"app.layout.modal.layoutToastLabel": "Canvis en la configuració del disseny",
"app.layout.modal.layoutSingular": "Disseny",
"app.layout.modal.layoutBtnDesc": "Definir el disseny com a opció seleccionada",
"app.layout.style.custom": "Personalitzar",
"app.layout.style.smart": "Disseny intel·ligent",
"app.layout.style.presentationFocus": "Centrar-se en la presentació",
@ -995,6 +1304,7 @@
"playback.player.thumbnails.wrapper.aria": "Àrea de miniatures",
"playback.player.webcams.wrapper.aria": "Área de cámaras web",
"app.learningDashboard.dashboardTitle": "Quadre de comandament d'anàlisi de l'aprenentatge",
"app.learningDashboard.bigbluebuttonTitle": "BigBlueButton",
"app.learningDashboard.downloadSessionDataLabel": "Descarregar dades de la sessió",
"app.learningDashboard.lastUpdatedLabel": "Actualitzat per última vegada a",
"app.learningDashboard.sessionDataDownloadedLabel": "Descarregat!",
@ -1019,6 +1329,12 @@
"app.learningDashboard.userDetails.response": "Resposta",
"app.learningDashboard.userDetails.mostCommonAnswer": "Resposta més comuna",
"app.learningDashboard.userDetails.anonymousAnswer": "Enquesta anònima",
"app.learningDashboard.userDetails.talkTime": "Temps de conversa",
"app.learningDashboard.userDetails.messages": "Missatges",
"app.learningDashboard.userDetails.emojis": "Emojis",
"app.learningDashboard.userDetails.raiseHands": "Mans aixecades",
"app.learningDashboard.userDetails.pollVotes": "Vots de l'enquesta",
"app.learningDashboard.userDetails.onlineIndicator": "{0} Hora de la connexió",
"app.learningDashboard.usersTable.title": "Resum",
"app.learningDashboard.usersTable.colOnline": "Hora de la connexió",
"app.learningDashboard.usersTable.colTalk": "Temps de conversa",
@ -1042,8 +1358,13 @@
"app.learningDashboard.pollsTable.anonymousRowName": "Anònim",
"app.learningDashboard.pollsTable.noPollsCreatedHeading": "No s'han creat enquestes",
"app.learningDashboard.pollsTable.noPollsCreatedMessage": "Una vegada que s'ha enviat una enquesta als usuaris, els seus resultats apareixeran en aquesta llista.",
"app.learningDashboard.pollsTable.answerTotal": "Total",
"app.learningDashboard.pollsTable.userLabel": "Usuari",
"app.learningDashboard.statusTimelineTable.title": "Línia de temps",
"app.learningDashboard.statusTimelineTable.thumbnail": "Presentació en miniatura.",
"app.learningDashboard.statusTimelineTable.presentation": "Presentació",
"app.learningDashboard.statusTimelineTable.pageNumber": "Pàgina",
"app.learningDashboard.statusTimelineTable.setAt": "Fixat en",
"app.learningDashboard.errors.invalidToken": "Token de sessió no vàlid",
"app.learningDashboard.errors.dataUnavailable": "Les dades ja no estan disponibles",
"mobileApp.portals.list.empty.addFirstPortal.label": "Afegeix el teu primer portal utilitzant el botó de dalt,",
@ -1057,6 +1378,4 @@
"mobileApp.portals.addPortalPopup.validation.emptyFields": "Camps obligatoris",
"mobileApp.portals.addPortalPopup.validation.portalNameAlreadyExists": "Nom ja utilitzat",
"mobileApp.portals.addPortalPopup.validation.urlInvalid": "Error en intentar carregar la pàgina - comprovi la URL i la connexió de xarxa"
}

View File

@ -7,6 +7,7 @@
"app.chat.locked": "Η συνομιλία είναι κλειδωμένη, απενεργοποιήθηκαν τα μηνύματα.",
"app.chat.inputLabel": "Εισαγωγή μηνύματος συνομιλίας {0}",
"app.chat.emojiButtonLabel": "Επιλογή Emoji",
"app.chat.loadMoreButtonLabel": "Φόρτωση περισσότερων",
"app.chat.inputPlaceholder": "Μήνυμα {0}",
"app.chat.titlePublic": "Δημόσια συνομιλία",
"app.chat.titlePrivate": "Ιδιωτική συνομιλία με {0}",
@ -24,8 +25,11 @@
"app.chat.breakoutDurationUpdated": "Ο χρόνος του τμήματος έχει οριστεί σε {0} λεπτά",
"app.chat.breakoutDurationUpdatedModerator": "Ο χρόνος των τμημάτων της αίθουσας είναι πλέον {0} λεπτά. Έχει σταλεί μια ειδοποίηση.",
"app.chat.emptyLogLabel": "Η καταγραφή της συνομιλίας είναι κενή",
"app.chat.away": "Είναι απών",
"app.chat.notAway": "Είναι παρών",
"app.chat.clearPublicChatMessage": "Το ιστορικό δημόσιας συνομιλίας έχει διαγραφεί από τον συντονιστή.",
"app.chat.multi.typing": "Πολλοί χρήστες πληκτρολογούν",
"app.chat.someone.typing": "Κάποιος πληκτρολογεί",
"app.chat.one.typing": "Ο/Η {0} πληκτρολογεί",
"app.chat.two.typing": "Οι {0} και {1} πληκτρολογούν",
"app.chat.copySuccess": "Το αρχείο καταγραφής της συνομιλίας αντιγράφηκε",
@ -36,6 +40,7 @@
"app.emojiPicker.clear": "Εκκαθάριση",
"app.emojiPicker.categories.label": "Κατηγορίες Emoji",
"app.emojiPicker.categories.people": "Άνθρωποι και Πρόσωπα",
"app.emojiPicker.categories.reactions": "Αντιδράσεις",
"app.emojiPicker.categories.nature": "Ζώα και Φύση",
"app.emojiPicker.categories.foods": "Φαγητά και Ποτά",
"app.emojiPicker.categories.places": "Ταξίδια και Τοποθεσίες",
@ -51,6 +56,23 @@
"app.emojiPicker.skintones.4": "Μεσαίος τόνος εμφάνισης",
"app.emojiPicker.skintones.5": "Μεσαίος-Σκοτεινός τόνος εμφάνισης",
"app.emojiPicker.skintones.6": "Σκοτεινός τόνος εμφάνισης",
"app.timer.title": "Χρόνος",
"app.timer.stopwatch.title": "Χρονόμετρο",
"app.timer.timer.title": "Χρόνος",
"app.timer.hideTimerLabel": "Απόκρυψη χρόνου",
"app.timer.button.stopwatch": "Χρονόμετρο",
"app.timer.button.timer": "Χρόνος",
"app.timer.button.start": "Έναρξη",
"app.timer.button.stop": "Λήξη",
"app.timer.button.reset": "Επαναφορά",
"app.timer.hours": "ώρες",
"app.timer.minutes": "λεπτά",
"app.timer.seconds": "δεύτερα",
"app.timer.songs": "Τραγούδια",
"app.timer.noTrack": "Κανένα τραγούδι",
"app.timer.track1": "Χαλαρωτικό",
"app.timer.track2": "Ηρεμία",
"app.timer.track3": "Χαρούμενο",
"app.captions.label": "Υπότιτλοι",
"app.captions.menu.close": "Κλείσιμο",
"app.captions.menu.start": "Έναρξη",
@ -102,6 +124,7 @@
"app.userList.messagesTitle": "Μηνύματα",
"app.userList.notesTitle": "Σημειώσεις",
"app.userList.notesListItem.unreadContent": "Νέο περιεχόμενο διαθέσιμο στην ενότητα κοινόχρηστων σημειώσεων",
"app.userList.timerTitle": "Χρόνος",
"app.userList.captionsTitle": "Υπότιτλοι",
"app.userList.presenter": "Παρουσιαστής",
"app.userList.you": "Εσείς",
@ -116,6 +139,8 @@
"app.userList.menuTitleContext": "Διαθέσιμες επιλογές",
"app.userList.chatListItem.unreadSingular": "Ένα νέο μήνυμα",
"app.userList.chatListItem.unreadPlural": "{0} νέα μηνύματα",
"app.userList.menu.away": "Θέστε τον εαυτό σας απών",
"app.userList.menu.notAway": "Θέστε τον εαυτό σας παρών",
"app.userList.menu.chat.label": "Έναρξη ιδιωτικής συνομιλίας",
"app.userList.menu.clearStatus.label": "Επαναφορά κατάστασης",
"app.userList.menu.removeUser.label": "Αφαίρεση χρήστη",
@ -140,6 +165,8 @@
"app.userList.userOptions.muteAllDesc": "Σίγαση όλων των χρηστών στη διάσκεψη",
"app.userList.userOptions.clearAllLabel": "Επαναφορά όλων των εικονιδίων κατάστασης",
"app.userList.userOptions.clearAllDesc": "Επαναφορά όλων των εικονιδίων κατάστασης των χρηστών.",
"app.userList.userOptions.clearAllReactionsLabel": "Εκκαθάριση όλων των αντιδράσεων",
"app.userList.userOptions.clearAllReactionsDesc": "Διαγράφει όλα τα emoji αντίδρασης από τους χρήστες",
"app.userList.userOptions.muteAllExceptPresenterLabel": "Σίγαση όλων των χρηστών εκτός του παρουσιαστή",
"app.userList.userOptions.muteAllExceptPresenterDesc": "Σίγαση όλων των χρηστών, εκτός του παρουσιαστή.",
"app.userList.userOptions.unmuteAllLabel": "Επαναφορά ήχου στη διάσκεψη.",
@ -156,6 +183,7 @@
"app.userList.userOptions.hideUserList": "Η λίστα χρήστη δεν είναι ορατή στους θεατές.",
"app.userList.userOptions.webcamsOnlyForModerator": "Μόνο οι συντονιστές μπορούν να δουν τις κάμερες των θεατών (λόγω των ρυθμίσεων κλειδώματος)",
"app.userList.content.participants.options.clearedStatus": "Επαναφορά κατάστασης όλων των χρηστών",
"app.userList.content.participants.options.clearedReactions": "Εκκαθάριση όλων των αντιδράσεων χρήστη",
"app.userList.userOptions.enableCam": "Οι κάμερες των θεατών είναι ενεργοποιημένες.",
"app.userList.userOptions.enableMic": "Τα μικρόφωνα των θεατών είναι ενεργοποιημένα.",
"app.userList.userOptions.enablePrivChat": "Ιδιωτική συνομιλία είναι ενεργοποιημένη",
@ -177,6 +205,11 @@
"app.media.screenshare.notSupported": "Η κοινή χρήση οθόνης δεν υποστηρίζεται από αυτόν τον περιηγητή σας.",
"app.media.screenshare.autoplayBlockedDesc": "Χρειαζόμαστε την άδειά σας για να σας προβάλουμε την οθόνη του παρουσιαστή.",
"app.media.screenshare.autoplayAllowLabel": "Προβολή διαμοιραζόμενης οθόνης",
"app.media.cameraAsContent.start": "Η τρέχουσα κάμερα έχει ξεκινήσει",
"app.media.cameraAsContent.end": "Η τρέχουσα κάμερα έχει σταματήσει",
"app.media.cameraAsContent.endDueToDataSaving": "Η τρέχουσα κάμερα έχει σταματήσει για εξοικονόμηση δεδομένων",
"app.media.cameraAsContent.autoplayBlockedDesc": "Χρειαζόμαστε την άδειά σας για να σας προβάλλουμε την κάμερα του παρουσιαστή.",
"app.media.cameraAsContent.autoplayAllowLabel": "Προβολή τρέχουσας κάμερας",
"app.screenshare.presenterLoadingLabel": "Ο διαμοιρασμός οθόνης φορτώνεται",
"app.screenshare.viewerLoadingLabel": "Η οθόνη παρουσιαστή φορτώνεται",
"app.screenshare.presenterSharingLabel": "Γίνεται διαμοιρασμός της οθόνης σας",
@ -185,6 +218,9 @@
"app.screenshare.screenshareRetryOtherEnvError": "Κωδικός {0}. Δεν είναι δυνατός ο διαμοιρασμός οθόνης. Δοκιμάστε ξανά από άλλη συσκευή ή φυλλομετρητή.",
"app.screenshare.screenshareUnsupportedEnv": "Κωδικός {0}. Ο φυλλομετρητής δεν υποστηρίζεται. Δοκιμάστε ξανά από άλλη συσκευή ή φυλλομετρητή.",
"app.screenshare.screensharePermissionError": "Κωδικός {0}. Για την καταγραφή της οθόνης απαιτείται εξουσιοδότηση.",
"app.cameraAsContent.presenterLoadingLabel": "Σύνδεση στην κάμερά σας",
"app.cameraAsContent.viewerLoadingLabel": "Η κάμερα παρουσιαστή φορτώνεται",
"app.cameraAsContent.presenterSharingLabel": "Τώρα προβάλλεται η κάμερά σας",
"app.meeting.ended": "Η συνεδρία έληξε",
"app.meeting.meetingTimeRemaining": "Υπολειπόμενος χρόνος σύσκεψης: {0}",
"app.meeting.meetingTimeHasEnded": "Ο χρόνος τελείωσε. Η σύσκεψη θα κλείσει σύντομα.",
@ -199,6 +235,7 @@
"app.presentation.hide": "Απόκρυψη παρουσίασης",
"app.presentation.notificationLabel": "Τρέχουσα παρουσίαση",
"app.presentation.downloadLabel": "Λήψη",
"app.presentation.actionsLabel": "Ενέργειες",
"app.presentation.slideContent": "Περιεχόμενα διαφάνειας",
"app.presentation.startSlideContent": "Έναρξη του περιεχομένου διαφάνειας",
"app.presentation.endSlideContent": "Τερματισμός του περιεχομένου διαφάνειας",
@ -250,8 +287,15 @@
"app.presentationUploader.sent": "Αποστολή",
"app.presentationUploader.exportingTimeout": "Η εξαγωγή διαρκεί αρκετά...",
"app.presentationUploader.export": "Αποστολή στο χώρο μηνυμάτων",
"app.presentationUploader.exportCurrentStatePresentation": "Στείλτε έναν σύνδεσμο λήψης για την παρουσίαση, συμπεριλαμβανομένων των σχολιασμών του πίνακα.",
"app.presentationUploader.enableOriginalPresentationDownload": "Ενεργοποίηση λήψης της παρουσίασης ({0})",
"app.presentationUploader.disableOriginalPresentationDownload": "Απενεργοποίηση λήψης της πρωτότυπης παρουσίασης ({0})",
"app.presentationUploader.dropdownExportOptions": "Επιλογές εξαγωγής",
"app.presentationUploader.export.linkAvailable": "Ο σύνδεσμος λήψης {0} είναι διαθέσιμος στο δημόσιο χώρο μηνυμάτων.",
"app.presentationUploader.export.downloadButtonAvailable": "Το κουμπί λήψης για την παρουσίαση {0} είναι διαθέσιμο.",
"app.presentationUploader.export.notAccessibleWarning": "ενδέχεται να μην είναι συμβατό με την προσβασιμότητα",
"app.presentationUploader.export.originalLabel": "Πρωτότυπο",
"app.presentationUploader.export.inCurrentStateLabel": "Στην τρέχουσα κατάσταση",
"app.presentationUploader.currentPresentationLabel": "Τρέχουσα παρουσίαση",
"app.presentationUploder.extraHint": "ΣΗΜΑΝΤΙΚΟ: κάθε αρχείο δεν πρέπει να ξεπερνά τα {0} ΜΒ και τις {1} σελίδες.",
"app.presentationUploder.uploadLabel": "Μεταφόρτωση",
@ -446,7 +490,10 @@
"app.actionsBar.actionsDropdown.minimizePresentationLabel": "Ελαχιστοποίηση παρουσίασης",
"app.actionsBar.actionsDropdown.minimizePresentationDesc": "Κουμπί για την ελαχιστοποίηση της παρουσίασης",
"app.actionsBar.actionsDropdown.layoutModal": "Κατάσταση ρυθμίσεων διάταξης",
"app.actionsBar.actionsDropdown.shareCameraAsContent": "Διαμοιρασμός κάμερας ως περιεχόμενο",
"app.actionsBar.actionsDropdown.unshareCameraAsContent": "Διακοπή διαμοιρασμού κάμερας ως περιεχόμενο",
"app.screenshare.screenShareLabel" : "Διαμοιρασμός οθόνης",
"app.cameraAsContent.cameraAsContentLabel" : "Τρέχουσα κάμερα",
"app.submenu.application.applicationSectionTitle": "Εφαρμογή",
"app.submenu.application.animationsLabel": "Γραφικά",
"app.submenu.application.audioFilterLabel": "Φίλτρα ήχου για το μικρόφωνο",
@ -460,6 +507,7 @@
"app.submenu.application.languageOptionLabel": "Επιλογή γλώσσας",
"app.submenu.application.noLocaleOptionLabel": "Κανένα ενεργό σετ γλώσσας",
"app.submenu.application.paginationEnabledLabel": "Σελιδοποίηση βίντεο",
"app.submenu.application.wakeLockEnabledLabel": "Μόνιμα ενεργό",
"app.submenu.application.layoutOptionLabel": "Τύπος διάταξης",
"app.submenu.application.pushLayoutLabel": "Επιβολή της διάταξης",
"app.submenu.application.localeDropdown.af": "Αφρικανικά",
@ -569,6 +617,8 @@
"app.talkingIndicator.moreThanMaxIndicatorsWereTalking" : "Μιλήσανε {0}+",
"app.talkingIndicator.wasTalking" : "Ο/Η {0} σταμάτησε να μιλάει",
"app.actionsBar.actionsDropdown.actionsLabel": "Ενέργειες",
"app.actionsBar.actionsDropdown.activateTimerStopwatchLabel": "Ενεργοποίηση χρόνου/χρονόμετρου",
"app.actionsBar.actionsDropdown.deactivateTimerStopwatchLabel": "Απενεργοποίηση χρόνου/χρονόμετρου",
"app.actionsBar.actionsDropdown.presentationLabel": "Μεταφόρτωση/Διαχείριση παρουσιάσεων",
"app.actionsBar.actionsDropdown.initPollLabel": "Ξεκινήστε μια δημοσκόπηση",
"app.actionsBar.actionsDropdown.desktopShareLabel": "Διαμοιρασμός της οθόνης σας",
@ -588,7 +638,10 @@
"app.actionsBar.actionsDropdown.takePresenterDesc": "Γίνετε ο νέος παρουσιαστής",
"app.actionsBar.actionsDropdown.selectRandUserLabel": "Επιλογή τυχαίου χρήστη",
"app.actionsBar.actionsDropdown.selectRandUserDesc": "Τυχαία επιλογή χρήστη από τους διαθέσιμους θεατές",
"app.actionsBar.actionsDropdown.propagateLayoutLabel": "Διάδοση διάταξης",
"app.actionsBar.reactions.reactionsButtonLabel": "Γραμμή κατάστασης αντιδράσεων",
"app.actionsBar.reactions.raiseHand": "Σηκώστε το χέρι σας",
"app.actionsBar.reactions.lowHand": "Κατεβάστε το χέρι σας",
"app.actionsBar.reactions.autoCloseReactionsBarLabel": "Αυτόματο κλείσιμο της γραμμής αντιδράσεων",
"app.actionsBar.emojiMenu.statusTriggerLabel": "Ορισμός κατάστασης",
"app.actionsBar.emojiMenu.awayLabel": "Μη διαθέσιμος",
"app.actionsBar.emojiMenu.awayDesc": "Αλλάξτε την κατάστασή σας σε απών",
@ -817,8 +870,14 @@
"app.toast.meetingMuteOn.label": "Όλοι οι χρήστες είναι σε σίγαση",
"app.toast.meetingMuteOnViewers.label": "Όλοι οι θεατές είναι σε σίγαση",
"app.toast.meetingMuteOff.label": "Η σίγαση της σύσκεψης απενεργοποιήθηκε.",
"app.toast.wakeLock.acquireSuccess": "Η επιλογή «μόνιμα ενεργό» είναι ενεργή. Μπορείτε να την απενεργοποιήσετε από τις επιλογές.",
"app.toast.wakeLock.acquireFailed": "Σφάλμα εφαρμογής της επιλογής «μόνιμα ενεργό».",
"app.toast.wakeLock.notSupported": "Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την επιλογή «μόνιμα ενεργό».",
"app.toast.wakeLock.disclaimer": "{0}. Θα τερματιστεί η κλήση όταν απενεργοποιηθεί η οθόνη σας.",
"app.toast.setEmoji.raiseHand": "Έχετε σηκώσει το χέρι σας",
"app.toast.setEmoji.lowerHand": "Η παλάμη σας έχει κατέβει",
"app.toast.setEmoji.away": "Ορίσατε την κατάστασή σας σε απών",
"app.toast.setEmoji.notAway": "Αφαιρέσατε την κατάστασή σας από απών",
"app.toast.promotedLabel": "Έχετε προαχθεί σε συντονιστή",
"app.toast.demotedLabel": "Έχετε υποβιβαστεί σε θεατή",
"app.notification.recordingStart": "Αυτή η διάσκεψη καταγράφεται",
@ -901,6 +960,7 @@
"app.lock-viewers.button.cancel": "Ακύρωση",
"app.lock-viewers.locked": "Κλειδωμένο",
"app.lock-viewers.hideViewersCursor": "Προβολή κέρσορα άλλων θεατών",
"app.lock-viewers.hideAnnotationsLabel": "Δείτε τα σχόλια των θεατών",
"app.guest-policy.ariaTitle": "Τυπικές ρυθμίσεις πολιτικής για επισκέπτη",
"app.guest-policy.title": "Πολιτική επισκεπτών",
"app.guest-policy.description": "Αλλαγή ρυθμίσεων της πολιτικής συνεδρίας για επισκέπτη",
@ -908,6 +968,7 @@
"app.guest-policy.button.alwaysAccept": "Πάντα αποδοχή",
"app.guest-policy.button.alwaysDeny": "Πάντα απόρριψη",
"app.guest-policy.policyBtnDesc": "Ορισμός πολιτικής συνεδρίας για επισκέπτες",
"app.guest-policy.feedbackMessage": "Η πολιτική επισκεπτών είναι:",
"app.connection-status.ariaTitle": "Τυπική κατάσταση σύνδεσης",
"app.connection-status.title": "Κατάσταση σύνδεσης",
"app.connection-status.description": "Προβολή κατάστασης σύνδεσης των χρηστών",
@ -965,6 +1026,7 @@
"app.videoPreview.webcamPreviewLabel": "Προεπισκόπηση κάμερας",
"app.videoPreview.webcamSettingsTitle": "Ρυθμίσεις κάμερας",
"app.videoPreview.webcamEffectsTitle": "Οπτικά εφέ κάμερας",
"app.videoPreview.cameraAsContentSettingsTitle": "Τρέχουσα κάμερα",
"app.videoPreview.webcamVirtualBackgroundLabel": "Ρυθμίσεις εικονικού υπόβαθρου",
"app.videoPreview.webcamVirtualBackgroundDisabledLabel": "Αυτή η συσκευή δεν υποστηρίζει φόντο εικονικής πραγματικότητας",
"app.videoPreview.webcamNotFoundLabel": "Δεν βρέθηκε κάμερα",
@ -1093,6 +1155,10 @@
"app.videoDock.webcamFocusDesc": "Εστίαση στην επιλεγμένη κάμερα.",
"app.videoDock.webcamUnfocusLabel": "Επαναφορά εστίασης",
"app.videoDock.webcamUnfocusDesc": "Επαναφορά εστίασης της επιλεγμένης κάμερας",
"app.videoDock.webcamDisableLabel": "Μη εμφάνιση κάμερας",
"app.videoDock.webcamDisableLabelAllCams": "Μη εμφάνιση κάμερας (όλες οι κάμερες).",
"app.videoDock.webcamEnableLabel": "Εμφάνιση κάμερας",
"app.videoDock.webcamDisableDesc": "Η μη εμφάνιση κάμερας απενεργοποιήθηκε",
"app.videoDock.webcamPinLabel": "Καρφίτσωμα",
"app.videoDock.webcamPinDesc": "Καρφίτσωμα επιλεγμένων καμερών",
"app.videoDock.webcamFullscreenLabel": "Κάμερα σε πλήρη οθόνη",
@ -1133,8 +1199,10 @@
"app.createBreakoutRoom.addRoomTime": "Αύξηση χρόνου του τμήματος αίθουσας διάσκεψης σε",
"app.createBreakoutRoom.addParticipantLabel": "+ Προσθήκη συμμετέχοντα",
"app.createBreakoutRoom.freeJoin": "Να επιτρέπεται οι χρήστες να επιλέγουν το τμήμα αίθουσας διάσκεψης που θα συμμετέχουν.",
"app.createBreakoutRoom.manageRoomsLabel": "Διαχείριση αιθουσών",
"app.createBreakoutRoom.captureNotes": "Αποθήκευση των κοινών σημειώσεων κατά τον τερματισμό του τμήματος αίθουσας",
"app.createBreakoutRoom.captureSlides": "Αποθήκευση ασπροπίνακα κατά τον τερματισμό του τμήματος αίθουσας",
"app.createBreakoutRoom.sendInvitationToMods": "Αποστολή πρόσκλησης σε καθορισμένους διαχειριστές",
"app.createBreakoutRoom.leastOneWarnBreakout": "Πρέπει να προσθέσετε τουλάχιστο έναν χρήστη στο τμήμα αίθουσας διάσκεψης.",
"app.createBreakoutRoom.minimumDurationWarnBreakout": "Η ελάχιστη διάρκεια του τμήματος αίθουσας διάσκεψης είναι {0} λεπτά.",
"app.createBreakoutRoom.modalDesc": "Συμβουλή: Μπορείτε να σύρετε το όνομα χρήστη για να τον προσθέσετε σε τμήμα αίθουσας διάσκεψης.",
@ -1180,10 +1248,9 @@
"app.debugWindow.form.chatLoggerLabel": "Δοκιμή επιπέδων καταγραφής συνομιλίας",
"app.debugWindow.form.button.apply": "Εφαρμογή",
"app.layout.modal.title": "Διάταξη",
"app.layout.modal.confirm": "Επιβεβαίωση",
"app.layout.modal.cancel": "Άκυρο",
"app.layout.modal.update": "Ενημέρωση",
"app.layout.modal.updateAll": "Ενημέρωση όλων",
"app.layout.modal.layoutLabel": "Επιλογή διάταξης",
"app.layout.modal.keepPushingLayoutLabel": "Επιβολή της διάταξης σε όλους",
"app.layout.modal.pushLayoutLabel": "Επιβολή σε όλους",
"app.layout.modal.layoutToastLabel": "Οι ρυθμίσεις διάταξης άλλαξαν",
"app.layout.modal.layoutSingular": "Διάταξη",

View File

@ -288,9 +288,9 @@
"app.presentationUploader.sent": "Sent",
"app.presentationUploader.exportingTimeout": "The export is taking too long...",
"app.presentationUploader.export": "Send to chat",
"app.presentationUploader.exportCurrentStatePresentation": "Send out a download link for the presentation in its current state",
"app.presentationUploader.enableOriginalPresentationDownload": "Enable download of the original presentation",
"app.presentationUploader.disableOriginalPresentationDownload": "Disable download of the original presentation",
"app.presentationUploader.exportCurrentStatePresentation": "Send out a download link for the presentation including whiteboard annotations",
"app.presentationUploader.enableOriginalPresentationDownload": "Enable download of the presentation ({0})",
"app.presentationUploader.disableOriginalPresentationDownload": "Disable download of the original presentation ({0})",
"app.presentationUploader.dropdownExportOptions": "Export options",
"app.presentationUploader.export.linkAvailable": "Link for downloading {0} available on the public chat.",
"app.presentationUploader.export.downloadButtonAvailable": "Download button for presentation {0} is available.",
@ -873,8 +873,8 @@
"app.toast.meetingMuteOff.label": "Meeting mute turned off",
"app.toast.wakeLock.acquireSuccess": "Wake lock active! You can turn it off under the settings menu.",
"app.toast.wakeLock.acquireFailed": "Error acquiring wake lock",
"app.toast.wakeLock.notSupported": "Wake lock not supported",
"app.toast.wakeLock.disclaimer": "{0}. You will be dropped from the call if the screen turns off.",
"app.toast.wakeLock.notSupported": "Your browser does not support wake locks",
"app.toast.wakeLock.disclaimer": "{0}. You will be dropped from the call after your screen turns off.",
"app.toast.setEmoji.raiseHand": "You have raised your hand",
"app.toast.setEmoji.lowerHand": "Your hand has been lowered",
"app.toast.setEmoji.away": "You have set your status to away",

View File

@ -287,14 +287,14 @@
"app.presentationUploader.sent": "Saadetud",
"app.presentationUploader.exportingTimeout": "Eksportimine võtab liiga kaua aega...",
"app.presentationUploader.export": "Saada vestlusesse",
"app.presentationUploader.exportCurrentStatePresentation": "Saada välja link esitluse allalaadimiseks selle praeguses olekus",
"app.presentationUploader.enableOriginalPresentationDownload": "Luba originaalesitluse allalaadimine",
"app.presentationUploader.disableOriginalPresentationDownload": "Keela originaalesitluse allalaadimine",
"app.presentationUploader.exportCurrentStatePresentation": "Saada välja link esitluse allalaadimiseks koos tahvlimärgetega.",
"app.presentationUploader.enableOriginalPresentationDownload": "Luba esitluse allalaadimine ({0})",
"app.presentationUploader.disableOriginalPresentationDownload": "Keela algse esitluse allalaadimine ({0})",
"app.presentationUploader.dropdownExportOptions": "Ekspordi valikud",
"app.presentationUploader.export.linkAvailable": "Faili {0} allalaadimise link on avalikus vestluses.",
"app.presentationUploader.export.downloadButtonAvailable": "Esitluse {0} allalaadimisnupp on saadaval.",
"app.presentationUploader.export.notAccessibleWarning": "ei tarvitse vastata ligipääsetavuse nõuetele",
"app.presentationUploader.export.originalLabel": "Originaal",
"app.presentationUploader.export.originalLabel": "Algne",
"app.presentationUploader.export.inCurrentStateLabel": "Praeguses olekus",
"app.presentationUploader.currentPresentationLabel": "Aktiivne esitlus",
"app.presentationUploder.extraHint": "TÄHELEPANU: ükski fail ei tohi suuruselt ületada {0} MB ja {1} lehte.",
@ -421,7 +421,7 @@
"app.polling.pollAnswerLabel": "Küsitluse vastus {0}",
"app.polling.pollAnswerDesc": "Vali see variant, et hääletada {0} poolt",
"app.failedMessage": "Vabandust! Serveriga ühenduse loomisel esineb probleeme.",
"app.downloadPresentationButton.label": "Laadi alla orginaalesitlus",
"app.downloadPresentationButton.label": "Laadi alla algne esitlus",
"app.connectingMessage": "Ühendumine...",
"app.waitingMessage": "Ühendus katkes. Proovin uuesti ühendust luua {0} sekundi pärast ...",
"app.retryNow": "Proovi kohe",
@ -870,11 +870,10 @@
"app.toast.meetingMuteOn.label": "Kõik kasutajad on vaigistatud",
"app.toast.meetingMuteOnViewers.label": "Kõik vaatajad on vaigistatud",
"app.toast.meetingMuteOff.label": "Koosoleku vaigistamine on välja lülitatud",
"app.toast.wakeLock.offerTitle": "Kas soovid, et su seadme ekraan jääks koosoleku ajal aktiivseks?",
"app.toast.wakeLock.offerAccept": "Jah!",
"app.toast.wakeLock.offerDecline": "Mitte praegu",
"app.toast.wakeLock.acquireSuccess": "Unelukk aktiivne! Saad selle sätete menüüs välja lülitada.",
"app.toast.wakeLock.acquireFailed": "Viga uneluku hankimisel.",
"app.toast.wakeLock.notSupported": "See brauser ei toeta unelukke.",
"app.toast.wakeLock.disclaimer": "{0}. Pärast ekraani väljalülitumist kõne katkeb.",
"app.toast.setEmoji.raiseHand": "Sa tõstsid käe",
"app.toast.setEmoji.lowerHand": "Sinu käsi langetati",
"app.toast.setEmoji.away": "Sa määrasid oma olekuks Eemal",

View File

@ -287,9 +287,9 @@
"app.presentationUploader.sent": "Enviado",
"app.presentationUploader.exportingTimeout": "A exportación está a tardar demasiado…",
"app.presentationUploader.export": "Enviar para a parola",
"app.presentationUploader.exportCurrentStatePresentation": "Enviar unha ligazón de descarga da presentación no seu estado actual",
"app.presentationUploader.enableOriginalPresentationDownload": "Activar a descarga da presentación orixinal",
"app.presentationUploader.disableOriginalPresentationDownload": "Desactivar a descarga da presentación orixinal",
"app.presentationUploader.exportCurrentStatePresentation": "Enviar unha ligazón de descarga da presentación incluídas as anotacións do encerado",
"app.presentationUploader.enableOriginalPresentationDownload": "Activar a descarga da presentación ({0})",
"app.presentationUploader.disableOriginalPresentationDownload": "Desactivar a descarga da presentación orixinal ({0})",
"app.presentationUploader.dropdownExportOptions": "Opcións de exportación",
"app.presentationUploader.export.linkAvailable": "Ligazón para descargar {0} dispoñíbel na parola pública.",
"app.presentationUploader.export.downloadButtonAvailable": "O botón de descarga para a presentación {0} está dispoñíbel",
@ -641,6 +641,7 @@
"app.actionsBar.reactions.reactionsButtonLabel": "Barra de reaccións",
"app.actionsBar.reactions.raiseHand": "Erguer a súa man",
"app.actionsBar.reactions.lowHand": "Baixar a súa man",
"app.actionsBar.reactions.autoCloseReactionsBarLabel": "Pechar automaticamente a barra de reaccións",
"app.actionsBar.emojiMenu.statusTriggerLabel": "Definir o estado",
"app.actionsBar.emojiMenu.awayLabel": "Ausente",
"app.actionsBar.emojiMenu.awayDesc": "Cambiar o seu estado a ausente",
@ -869,11 +870,10 @@
"app.toast.meetingMuteOn.label": "Todos os usuarios foron silenciados",
"app.toast.meetingMuteOnViewers.label": "Todos os espectadores foron silenciados",
"app.toast.meetingMuteOff.label": "Desactivouse o silencio da xuntanza",
"app.toast.wakeLock.offerTitle": "Quere manter activa a pantalla do eu dispositivo durante a xuntanza?",
"app.toast.wakeLock.offerAccept": "Si!",
"app.toast.wakeLock.offerDecline": "Agora non",
"app.toast.wakeLock.acquireSuccess": "O «bloqueo de apagado da pantalla» está activo! Pode desactivalo no menú de axustes.",
"app.toast.wakeLock.acquireFailed": "Produciuse un erro ao obter o «bloqueo de apagado da pantalla».",
"app.toast.wakeLock.acquireFailed": "Produciuse un erro ao obter o «bloqueo de apagado da pantalla»",
"app.toast.wakeLock.notSupported": "O «bloqueo de apagado da pantalla» non é compatíbel",
"app.toast.wakeLock.disclaimer": "{0}. Se a pantalla se apaga, será eliminado da chamada.",
"app.toast.setEmoji.raiseHand": "Vde. ergueu a man",
"app.toast.setEmoji.lowerHand": "A súa man foi baixada",
"app.toast.setEmoji.away": "Vde. definiu o seu estado como ausente",

View File

@ -287,9 +287,9 @@
"app.presentationUploader.sent": "送信終了",
"app.presentationUploader.exportingTimeout": "書き出しに時間がかかりすぎています...",
"app.presentationUploader.export": "チャットへ送信",
"app.presentationUploader.exportCurrentStatePresentation": "書き込みを含めてプレゼンをダウンロードするためのリンクを送付",
"app.presentationUploader.enableOriginalPresentationDownload": "書き込みのない元のプレゼンをダウンロード可能にする",
"app.presentationUploader.disableOriginalPresentationDownload": "プレゼンのダウンロードを許可しない",
"app.presentationUploader.exportCurrentStatePresentation": "ホワイトボードの書き込みを含めたプレゼンファイルのダウンロードリンクを送る",
"app.presentationUploader.enableOriginalPresentationDownload": "プレゼン({0})のダウンロードを有効にする",
"app.presentationUploader.disableOriginalPresentationDownload": "元のプレゼン({0})のダウンロードを無効にする",
"app.presentationUploader.dropdownExportOptions": "書き出しオプション",
"app.presentationUploader.export.linkAvailable": "{0}をダウンロードするためのリンクが、公開チャットから利用できます。",
"app.presentationUploader.export.downloadButtonAvailable": "プレゼン {0}をダウンロードするためのボタンが利用できます。",
@ -870,11 +870,10 @@
"app.toast.meetingMuteOn.label": "全てのユーザーがミュートされました",
"app.toast.meetingMuteOnViewers.label": "すべての視聴者はミュートされました",
"app.toast.meetingMuteOff.label": "会議のミュートを解除しました",
"app.toast.wakeLock.offerTitle": "会議中、画面スリープ機能を停止しますか?",
"app.toast.wakeLock.offerAccept": "はい!",
"app.toast.wakeLock.offerDecline": "今はやめておく",
"app.toast.wakeLock.acquireSuccess": "画面スリープを停止しています!設定メニューから再開することができます。",
"app.toast.wakeLock.acquireFailed": "画面スリープの停止に失敗しました。",
"app.toast.wakeLock.acquireFailed": "スリープの停止に失敗しました",
"app.toast.wakeLock.notSupported": "スリープの停止はサポートされていません",
"app.toast.wakeLock.disclaimer": "{0}。画面をオフにすると呼び出しを見落とす可能性があります。",
"app.toast.setEmoji.raiseHand": "手をあげました",
"app.toast.setEmoji.lowerHand": "手をおろしました",
"app.toast.setEmoji.away": "ステータスを退席中にしました",

View File

@ -39,6 +39,7 @@ class Audio extends MultiUsers {
await this.modPage.waitAndClick(e.joinAudio);
await connectMicrophone(this.modPage);
await this.modPage.hasElement(e.isTalking);
await this.modPage.waitAndClick(e.muteMicButton);
await this.modPage.wasRemoved(e.isTalking);
await this.modPage.hasElement(e.wasTalking);
@ -73,6 +74,7 @@ class Audio extends MultiUsers {
await this.modPage.waitAndClick(e.unmuteMicButton);
await this.modPage.hasElement(e.isTalking);
}
await this.modPage.hasElement(e.isTalking);
await this.modPage.waitAndClick(e.muteMicButton);
await this.modPage.hasElement(e.wasTalking);
await this.modPage.wasRemoved(e.muteMicButton);
@ -101,6 +103,7 @@ class Audio extends MultiUsers {
}
await this.modPage.waitAndClick(e.talkingIndicator);
await this.modPage.hasElement(e.wasTalking);
await this.modPage.hasElement(e.wasTalking);
await this.modPage.wasRemoved(e.muteMicButton);
await this.modPage.hasElement(e.unmuteMicButton);
await this.modPage.wasRemoved(e.isTalking);

View File

@ -265,6 +265,8 @@ exports.answerE = 'div[data-test="numberOfVotes"]>>nth=4';
// Presentation
exports.currentSlideImg = 'img[id="slide-background-shape_image"]';
exports.uploadPresentationFileName = 'uploadTest.png';
exports.presentationPPTX = 'BBB.pptx';
exports.presentationTXT = 'helloWorld.txt';
exports.presentationPlaceholderLabel = 'There is no currently active presentation';
exports.noPresentationLabel = 'There is no currently active presentation';
exports.startScreenSharing = 'button[data-test="startScreenShare"]';
@ -370,7 +372,8 @@ exports.error403removedLabel = 'You have been removed from the meeting';
exports.removeUser = 'li[data-test="removeUser"]';
exports.removeUserConfirmationBtn = 'button[data-test="removeUserConfirmation"]';
exports.confirmationCheckbox = 'input[id="confirmationCheckbox"]';
exports.userBannedMessage = 'div[id="app"] >> div >> div:nth-child(2)';
exports.userBannedMessage1 = 'div[id="app"] >> div >> div:nth-child(1)';
exports.userBannedMessage2 = 'div[id="app"] >> div >> div:nth-child(2)';
exports.meetingEndedModalTitle = 'h1[data-test="meetingEndedModalTitle"]';
exports.unmuteUser = 'li[data-test="unmuteUser"]';
exports.ejectCamera = 'li[data-test="ejectCamera"]';
@ -472,6 +475,12 @@ exports.wbColorRed = 'button[id="TD-Styles-Color-Swatch-red"]';
exports.wbFillDrawing = 'button[id="TD-Styles-Fill"]';
exports.wbDashDotted = 'div[id="TD-Styles-Dash-dotted"]';
exports.wbSizeLarge = 'div[id="TD-Styles-Dash-large"]';
exports.wbOptions = 'button[id="TD-Tools-Dots"]';
exports.wbDuplicate = 'span[id="TD-Tools-Copy"]';
exports.wbRotate = 'span[id="TD-Tools-Rotate"]';
exports.wbMoveBackward = 'span[id="TD-Tools-ArrowDown"]';
exports.wbMoveForward = 'span[id="TD-Tools-ArrowUp"]';
exports.wbMoveToFront = 'span[id="TD-Tools-PinTop"]';
exports.wbPaste = 'button[id="TD-ContextMenu-Paste"]';
// About modal
@ -480,7 +489,7 @@ exports.aboutModal = 'div[data-test="aboutModalTitleLabel"]';
// Help button
exports.helpButton = 'li[data-test="helpButton"]';
exports.helpPageTitle = 'BigBlueButton Tutorials | Built For Teachers | BigBlueButton'
exports.helpPageTitle = 'BigBlueButton Tutorials | Built For Teachers | BigBlueButton';
// Dark mode
exports.darkModeToggleBtn = 'input[data-test="darkModeToggleBtn"]';

Binary file not shown.

View File

@ -0,0 +1 @@
Hello World!!!

View File

@ -7,6 +7,7 @@ const helpers = require('./helpers');
const e = require('./elements');
const { env } = require('node:process');
const { ELEMENT_WAIT_TIME, ELEMENT_WAIT_LONGER_TIME, VIDEO_LOADING_WAIT_TIME } = require('./constants');
const { recordMeeting } = require('../parameters/constants');
const { checkElement, checkElementLengthEqualTo } = require('./util');
const { generateSettingsData, getSettings } = require('./settings');
@ -27,7 +28,7 @@ class Page {
}
async init(isModerator, shouldCloseAudioModal, initOptions) {
const { fullName, meetingId, createParameter, joinParameter, customMeetingId } = initOptions || {};
const { fullName, meetingId, createParameter, joinParameter, customMeetingId, isRecording } = initOptions || {};
if (!isModerator) this.initParameters.moderatorPW = '';
if (fullName) this.initParameters.fullName = fullName;
@ -43,6 +44,7 @@ class Page {
await expect(hasErrorLabel, 'Getting error when joining. Check if the BBB_URL and BBB_SECRET are set correctly').toBeFalsy();
this.settings = await generateSettingsData(this.page);
const { autoJoinAudioModal } = this.settings;
if (isRecording && !isModerator) await this.closeRecordingModal();
if (shouldCloseAudioModal && autoJoinAudioModal) await this.closeAudioModal();
}
@ -129,6 +131,11 @@ class Page {
await this.waitAndClick(e.closeModal);
}
async closeRecordingModal() {
await this.waitForSelector(e.simpleModal, ELEMENT_WAIT_LONGER_TIME);
await this.waitAndClick(e.confirmRecording);
}
async waitForSelector(selector, timeout = ELEMENT_WAIT_TIME) {
await this.page.waitForSelector(selector, { timeout });
}

View File

@ -13,8 +13,8 @@ test.describe.serial("Layout management", () => {
test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await layouts.initModPage(page, true, { customParameter: hidePresentationToast, customMeetingId: CUSTOM_MEETING_ID });
await layouts.initUserPage(true, context, { customParameter: hidePresentationToast });
await layouts.initModPage(page, true, { createParameter: hidePresentationToast, customMeetingId: CUSTOM_MEETING_ID });
await layouts.initUserPage(true, context, { createParameter: hidePresentationToast });
await layouts.modPage.shareWebcam();
await layouts.userPage.shareWebcam();
});

View File

@ -126,7 +126,7 @@ class LearningDashboard extends MultiUsers {
const timeContent = await (timeLocator).textContent();
const array = timeContent.split(':').map(Number);
const firstTime = array[1] * 3600 + array[2] * 60 + array[3];
await sleep(5000);
await sleep(10000);
await this.dashboardPage.reloadPage();
const timeContentGreater = await (timeLocator).textContent();
const arrayGreater = timeContentGreater.split(':').map(Number);

View File

@ -7,7 +7,7 @@ test.describe.serial('Learning Dashboard', async () => {
test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await learningDashboard.initModPage(page, true, { customParameter: c.recordMeeting });
await learningDashboard.initModPage(page, true, { createParameter: c.recordMeeting });
await learningDashboard.getDashboardPage(context);
});
@ -20,7 +20,7 @@ test.describe.serial('Learning Dashboard', async () => {
});
test('Polls @ci', async ({ context }) => {
await learningDashboard.initUserPage(true, context);
await learningDashboard.initUserPage(true, context, { isRecording: true });
await learningDashboard.polls();
});

View File

@ -58,6 +58,7 @@ class Notifications extends MultiUsers {
await this.modPage.waitAndClick(e.reactionsButton);
await this.modPage.waitAndClick(e.raiseHandBtn);
await sleep(1000);
await this.modPage.waitAndClick(e.reactionsButton);
await this.modPage.waitAndClick(e.lowerHandBtn);
await this.modPage.wasRemoved(e.raiseHandRejection);
await util.checkNotificationText(this.modPage, e.raisingHandToast);

View File

@ -47,22 +47,22 @@ test.describe.parallel('Notifications', () => {
test.describe.parallel('Recording', () => {
test('Notification appearing when user is not in audio', async ({ browser, page }) => {
const recordingNotifications = new RecordingNotifications(browser, page);
await recordingNotifications.init(true, true, { customParameter: c.recordMeeting });
await recordingNotifications.init(true, true, { createParameter: c.recordMeeting });
await recordingNotifications.notificationNoAudio();
});
test('Notification appearing when user is in listen only', async ({ browser, page }) => {
const recordingNotifications = new RecordingNotifications(browser, page);
await recordingNotifications.init(true, true, { customParameter: c.recordMeeting });
await recordingNotifications.init(true, true, { createParameter: c.recordMeeting });
await recordingNotifications.notificationListenOnly();
});
test('No notification appearing when user is in audio', async ({ browser, page }) => {
const recordingNotifications = new RecordingNotifications(browser, page);
await recordingNotifications.init(true, true, { customParameter: c.recordMeeting });
await recordingNotifications.init(true, true, { createParameter: c.recordMeeting });
await recordingNotifications.noNotificationInAudio();
});
test('Modal appearing when user wants to start recording', async ({ browser, page }) => {
const recordingNotifications = new RecordingNotifications(browser, page);
await recordingNotifications.init(true, true, { customParameter: c.recordMeeting });
await recordingNotifications.init(true, true, { createParameter: c.recordMeeting });
await recordingNotifications.modalStartRecording();
});
});

View File

@ -366,7 +366,7 @@ test.describe.parallel('Custom Parameters', () => {
test('Display Branding Area', async ({ browser, context, page }) => {
const customParam = new CustomParameters(browser, context);
await customParam.initModPage(page, true, { joinParameter: `${c.displayBrandingArea}&${encodeCustomParams(c.logo)}` });
await customParam.initModPage(page, true, { createParameter: `${c.displayBrandingArea}&${encodeCustomParams(c.logo)}` });
await customParam.displayBrandingArea();
});
@ -506,7 +506,7 @@ test.describe.parallel('Custom Parameters', () => {
test('Multi Users Pen Only', async ({ browser, context, page }) => {
const customParam = new CustomParameters(browser, context);
await customParam.initModPage(page, true, { joinParameter: c.multiUserPenOnly });
await customParam.initUserPage(true, context, { useModMeetingId: true, customParameter: c.multiUserPenOnly });
await customParam.initUserPage(true, context, { useModMeetingId: true, createParameter: c.multiUserPenOnly });
await customParam.multiUserPenOnly();
});

View File

@ -1,6 +1,5 @@
const { test } = require('@playwright/test');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants.js');
const e = require('../core/elements.js');
const { getSettings } = require('../core/settings.js');

View File

@ -66,6 +66,7 @@ class Presentation extends MultiUsers {
async uploadSinglePresentationTest() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await this.modPage.waitForSelector(e.skipSlide);
await this.modPage.wasRemoved(e.smallToastMsg, ELEMENT_WAIT_EXTRA_LONG_TIME);
await uploadSinglePresentation(this.modPage, e.pdfFileName, UPLOAD_PDF_WAIT_TIME);
// wait until the notifications disappear
@ -85,6 +86,50 @@ class Presentation extends MultiUsers {
});
}
async uploadOtherPresentationsFormat() {
await uploadSinglePresentation(this.modPage, e.uploadPresentationFileName, UPLOAD_PDF_WAIT_TIME);
await this.modPage.waitAndClick(e.smallToastMsg);
await this.modPage.wasRemoved(e.smallToastMsg, ELEMENT_WAIT_LONGER_TIME);
await this.userPage.wasRemoved(e.presentationStatusInfo);
await this.userPage.wasRemoved(e.smallToastMsg);
const modWhiteboardLocator = this.modPage.getLocator(e.whiteboard);
const userWhiteboardLocator = this.userPage.getLocator(e.whiteboard);
await expect(modWhiteboardLocator).toHaveScreenshot('moderator-png-presentation-screenshot.png', {
maxDiffPixels: 1000,
});
await expect(userWhiteboardLocator).toHaveScreenshot('viewer-png-presentation-screenshot.png', {
maxDiffPixels: 1000,
});
await uploadSinglePresentation(this.modPage, e.presentationPPTX, UPLOAD_PDF_WAIT_TIME);
await this.modPage.waitAndClick(e.smallToastMsg);
await this.modPage.wasRemoved(e.smallToastMsg, ELEMENT_WAIT_LONGER_TIME);
await this.userPage.wasRemoved(e.presentationStatusInfo);
await this.userPage.wasRemoved(e.smallToastMsg);
await expect(modWhiteboardLocator).toHaveScreenshot('moderator-pptx-presentation-screenshot.png', {
maxDiffPixels: 1000,
});
await expect(userWhiteboardLocator).toHaveScreenshot('viewer-pptx-presentation-screenshot.png', {
maxDiffPixels: 1000,
});
await uploadSinglePresentation(this.modPage, e.presentationTXT, UPLOAD_PDF_WAIT_TIME);
await this.modPage.waitAndClick(e.smallToastMsg);
await this.modPage.wasRemoved(e.smallToastMsg, ELEMENT_WAIT_LONGER_TIME);
await this.userPage.wasRemoved(e.presentationStatusInfo);
await this.userPage.wasRemoved(e.smallToastMsg);
await expect(modWhiteboardLocator).toHaveScreenshot('moderator-txt-presentation-screenshot.png', {
maxDiffPixels: 1000,
});
await expect(userWhiteboardLocator).toHaveScreenshot('viewer-txt-presentation-screenshot.png', {
maxDiffPixels: 1000,
});
}
async uploadMultiplePresentationsTest() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
@ -162,7 +207,7 @@ class Presentation extends MultiUsers {
await this.modPage.waitAndClick(e.sendPresentationInCurrentStateBtn);
await this.modPage.hasElement(e.downloadPresentationToast);
await this.modPage.hasElement(e.smallToastMsg, ELEMENT_WAIT_EXTRA_LONG_TIME);
await this.userPage.hasElement(e.downloadPresentation);
await this.userPage.hasElement(e.downloadPresentation, ELEMENT_WAIT_EXTRA_LONG_TIME);
const downloadPresentationLocator = this.userPage.getLocator(e.downloadPresentation);
await this.userPage.handleDownload(downloadPresentationLocator, testInfo);
}

View File

@ -29,7 +29,7 @@ test.describe.parallel('Presentation', () => {
// https://docs.bigbluebutton.org/2.6/release-tests.html#fit-to-width-option
test('Presentation fit to width @ci @flaky', async ({ browser, context, page }) => {
const presentation = new Presentation(browser, context);
await presentation.initModPage(page, true, { customParameter: customStyleAvoidUploadingNotifications });
await presentation.initModPage(page, true, { createParameter: customStyleAvoidUploadingNotifications });
await presentation.initUserPage(true, context);
await presentation.fitToWidthTest();
});
@ -76,6 +76,12 @@ test.describe.parallel('Presentation', () => {
await presentation.uploadSinglePresentationTest();
});
test('Upload Other Presentations Format @ci', async ({ browser, context, page }) => {
const presentation = new Presentation(browser, context);
await presentation.initPages(page, true);
await presentation.uploadOtherPresentationsFormat();
});
// 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);
@ -110,7 +116,7 @@ test.describe.parallel('Presentation', () => {
test('Remove previous presentation from previous presenter', async ({ browser, context, page }) => {
const presentation = new Presentation(browser, context);
await presentation.initModPage(page, true, { customParameter: customStyleAvoidUploadingNotifications });
await presentation.initModPage(page, true, { createParameter: customStyleAvoidUploadingNotifications });
await presentation.initUserPage(true, context);
await presentation.removePreviousPresentationFromPreviousPresenter();
});

View File

@ -32,7 +32,7 @@ async function uploadSinglePresentation(test, fileName, uploadTimeout = UPLOAD_P
await test.hasText('body', e.statingUploadPresentationToast);
await test.waitAndClick(e.confirmManagePresentation);
await test.hasElement(e.presentationStatusInfo, ELEMENT_WAIT_LONGER_TIME);
await test.hasElement(e.presentationUploadProgressToast, ELEMENT_WAIT_LONGER_TIME);
await test.page.waitForFunction(([selector, firstSlideSrc]) => {
const currentSrc = document.querySelector(selector).src;
return currentSrc != firstSlideSrc;

View File

@ -58,7 +58,7 @@ class SharedNotes extends MultiUsers {
await notesLocator.type(e.message);
await notesLocator.press('Control+Z');
await expect(notesLocator).toBeEmpty();
await expect(notesLocator).toContainText('');
await notesLocator.press('Control+Y');
await expect(notesLocator).toContainText(e.message);
@ -107,7 +107,7 @@ class SharedNotes extends MultiUsers {
const html = await this.modPage.handleDownload(exportHtmlLocator, testInfo);
const htmlFileExtension = (html.download._suggestedFilename).split('.').pop();
await checkTextContent(htmlFileExtension, 'html');
await checkTextContent(html.content, e.message);
await checkTextContent(html.content, e.message);
//.etherpad checks
const etherpad = await this.modPage.handleDownload(exportEtherpadLocator, testInfo);

View File

@ -147,17 +147,18 @@ class MultiUsers {
await this.initUserPage();
await this.userPage.waitAndClick(e.reactionsButton);
await this.userPage.waitAndClick(e.raiseHandBtn);
await this.userPage.waitAndClick(e.reactionsButton);
await this.userPage.hasElement(e.lowerHandBtn);
await this.modPage.comparingSelectorsBackgroundColor(e.avatarsWrapperAvatar, `${e.userListItem} > div ${e.userAvatar}`);
await sleep(1000);
await this.userPage.waitAndClick(e.lowerHandBtn);
await this.userPage.waitAndClick(e.reactionsButton);
await this.userPage.hasElement(e.raiseHandBtn);
}
async raiseHandRejected() {
const { reactionsButton } = getSettings();
if (!reactionsButton) {
console.log('=== inside')
await this.modPage.waitForSelector(e.whiteboard);
await this.modPage.hasElement(e.joinAudio);
await this.modPage.wasRemoved(e.reactionsButton);
@ -168,6 +169,7 @@ class MultiUsers {
await this.initUserPage();
await this.userPage.waitAndClick(e.reactionsButton);
await this.userPage.waitAndClick(e.raiseHandBtn);
await this.userPage.waitAndClick(e.reactionsButton);
await this.userPage.hasElement(e.lowerHandBtn);
await this.userPage.press('Escape');
await this.modPage.comparingSelectorsBackgroundColor(e.avatarsWrapperAvatar, `${e.userListItem} > div ${e.userAvatar}`);
@ -349,16 +351,22 @@ class MultiUsers {
await this.modPage.waitAndClick(e.removeUserConfirmationBtn);
await this.modPage.wasRemoved(e.userListItem);
//Will be modified when the issue is fixed and accept just one of both screens
//https://github.com/bigbluebutton/bigbluebutton/issues/16463
// Will be modified when the issue is fixed and accept just one of both screens
// https://github.com/bigbluebutton/bigbluebutton/issues/16463
try {
await this.modPage2.hasElement(e.errorScreenMessage);
} catch (err) {
} catch {
await this.modPage2.hasElement(e.meetingEndedModalTitle);
}
await this.initModPage2(false, context, {meetingId: this.modPage.meetingId, customParameter: 'userID=Moderator2'})
await this.modPage2.hasText(e.userBannedMessage, /banned/);
await this.initModPage2(false, context, { meetingId: this.modPage.meetingId, joinParameter: 'userID=Moderator2' });
// Due to same reason above, sometimes it displays different messages
try {
await this.modPage2.hasText(e.userBannedMessage2, /banned/);
} catch {
await this.modPage2.hasText(e.userBannedMessage1, /removed/);
}
}
async writeClosedCaptions() {

View File

@ -88,7 +88,7 @@ test.describe.parallel('User', () => {
test('Remove user and prevent rejoining', async ({ browser, context, page }) => {
const multiusers = new MultiUsers(browser, context);
await multiusers.initModPage(page, true);
await multiusers.initModPage2(true, context, { customParameter: 'userID=Moderator2' });
await multiusers.initModPage2(true, context, { joinParameter: 'userID=Moderator2' });
await multiusers.removeUserAndPreventRejoining(context);
});
});
@ -278,6 +278,7 @@ test.describe.parallel('User', () => {
});
test('User List should not appear when Chat Panel or Whiteboard are active on mobile devices', async ({ browser }) => {
test.fixme();
const iphoneContext = await browser.newContext({ ...iPhone11 });
const motoContext = await browser.newContext({ ...motoG4 });
const modPage = await iphoneContext.newPage();

View File

@ -43,7 +43,9 @@ test.describe.parallel('Webcam', () => {
});
test.describe('Webcam background', () => {
test('Select one of the default backgrounds @ci', async ({ browser, page }) => {
/* this test has the flaky tag because it is breaking due to a default video from chrome that
is overlapping the virtual background. */
test('Select one of the default backgrounds @ci @flaky', async ({ browser, page }) => {
const webcam = new Webcam(browser, page);
await webcam.init(true, true);
await webcam.applyBackground();

View File

@ -16,9 +16,12 @@ class Pan extends MultiUsers {
const screenshotOptions = {
maxDiffPixels: 1000,
};
const zoomResetBtn = this.modPage.getLocator(e.resetZoomButton);
for(let i = 100; i < 200; i += 25) {
const currentZoomLabel = await zoomResetBtn.textContent();
await this.modPage.waitAndClick(e.zoomInButton);
await expect(zoomResetBtn).not.toContainText(currentZoomLabel);
}
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);

View File

@ -0,0 +1,108 @@
const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const { sleep } = require('../core/helpers');
class ShapeOptions extends MultiUsers {
constructor(browser, context) {
super(browser, context);
}
async duplicate() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
const modWbLocator = this.modPage.getLocator(e.whiteboard);
const wbBox = await modWbLocator.boundingBox();
const screenshotOptions = {
maxDiffPixels: 1000,
};
await this.modPage.waitAndClick(e.wbShapesButton);
await this.modPage.waitAndClick(e.wbRectangleShape);
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
await this.modPage.page.mouse.click(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.waitAndClick(e.wbOptions);
await this.modPage.waitAndClick(e.wbDuplicate);
await this.modPage.waitAndClick(e.wbOptions);
await expect(modWbLocator).toHaveScreenshot('moderator-duplicate.png', screenshotOptions);
const userWbLocator = this.userPage.getLocator(e.whiteboard);
await expect(userWbLocator).toHaveScreenshot('viewer-duplicate.png', screenshotOptions);
}
async rotate() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
const modWbLocator = this.modPage.getLocator(e.whiteboard);
const wbBox = await modWbLocator.boundingBox();
const screenshotOptions = {
maxDiffPixels: 1000,
};
await this.modPage.waitAndClick(e.wbShapesButton);
await this.modPage.waitAndClick(e.wbRectangleShape);
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
await this.modPage.page.mouse.click(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.waitAndClick(e.wbOptions);
await this.modPage.waitAndClick(e.wbRotate);
await this.modPage.waitAndClick(e.wbOptions);
await expect(modWbLocator).toHaveScreenshot('moderator-rotate.png', screenshotOptions);
const userWbLocator = this.userPage.getLocator(e.whiteboard);
await expect(userWbLocator).toHaveScreenshot('viewer-rotate.png', screenshotOptions);
}
async movingShape() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
const modWbLocator = this.modPage.getLocator(e.whiteboard);
const wbBox = await modWbLocator.boundingBox();
const screenshotOptions = {
maxDiffPixels: 1000,
};
await this.modPage.waitAndClick(e.wbShapesButton);
await this.modPage.waitAndClick(e.wbRectangleShape);
await this.modPage.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
await this.modPage.page.mouse.click(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.waitAndClick(e.wbStyles);
await this.modPage.waitAndClick(e.wbFillDrawing);
await this.modPage.waitAndClick(e.wbStyles);
await this.modPage.waitAndClick(e.wbOptions);
await this.modPage.waitAndClick(e.wbDuplicate);
await this.modPage.waitAndClick(e.wbMoveBackward);
await expect(modWbLocator).toHaveScreenshot('moderator-move-backward.png', screenshotOptions);
const userWbLocator = this.userPage.getLocator(e.whiteboard);
await expect(userWbLocator).toHaveScreenshot('viewer-move-backward.png', screenshotOptions);
await this.modPage.waitAndClick(e.wbMoveForward);
await expect(modWbLocator).toHaveScreenshot('moderator-move-forward.png', screenshotOptions);
await expect(userWbLocator).toHaveScreenshot('viewer-move-forward.png', screenshotOptions);
await this.modPage.waitAndClick(e.wbMoveBackward);
await this.modPage.waitAndClick(e.wbMoveToFront);
await expect(modWbLocator).toHaveScreenshot('moderator-move-to-front.png', screenshotOptions);
await expect(userWbLocator).toHaveScreenshot('viewer-move-to-front.png', screenshotOptions);
}
}
exports.ShapeOptions = ShapeOptions;

View File

@ -7,7 +7,6 @@ const { DrawLine } = require('./drawLine');
const { DrawPencil } = require('./drawPencil');
const { DrawText } = require('./drawText');
const { DrawStickyNote } = require('./drawStickyNote');
const { Zoom } = require('./zoom');
const { Pan } = require('./pan');
const { Eraser } = require('./eraser');
const { DrawArrow } = require('./drawArrow');
@ -19,6 +18,7 @@ const { UndoDrawing } = require('./undoDraw');
const { RedoDrawing } = require('./redoDraw');
const { ChangeStyles } = require('./changeStyles');
const { RealTimeText } = require('./realTimeText');
const { ShapeOptions } = require('./shapeOptions');
const hidePresentationToast = encodeCustomParams(PARAMETER_HIDE_PRESENTATION_TOAST);
@ -44,134 +44,150 @@ test.describe.parallel('Whiteboard tools - visual regression', () => {
test('Draw rectangle', async ({ browser, context, page }) => {
const drawRectangle = new DrawRectangle(browser, context);
await drawRectangle.initModPage(page, true, { customMeetingId: 'draw_rectangle_meeting', customParameter: hidePresentationToast });
await drawRectangle.initUserPage(true, context, { customParameter: hidePresentationToast });
await drawRectangle.initModPage(page, true, { customMeetingId: 'draw_rectangle_meeting', createParameter: hidePresentationToast });
await drawRectangle.initUserPage(true, context, { createParameter: hidePresentationToast });
await drawRectangle.test();
});
test('Draw ellipse', async ({ browser, context, page }) => {
const drawEllipse = new DrawEllipse(browser, context);
await drawEllipse.initModPage(page, true, { customMeetingId: 'draw_ellipse_meeting', customParameter: hidePresentationToast });
await drawEllipse.initUserPage(true, context, { customParameter: hidePresentationToast });
await drawEllipse.initModPage(page, true, { customMeetingId: 'draw_ellipse_meeting', createParameter: hidePresentationToast });
await drawEllipse.initUserPage(true, context, { createParameter: hidePresentationToast });
await drawEllipse.test();
});
test('Draw triangle', async ({ browser, context, page }) => {
const drawTriangle = new DrawTriangle(browser, context);
await drawTriangle.initModPage(page, true, { customMeetingId: 'draw_triangle_meeting', customParameter: hidePresentationToast });
await drawTriangle.initUserPage(true, context, { customParameter: hidePresentationToast });
await drawTriangle.initModPage(page, true, { customMeetingId: 'draw_triangle_meeting', createParameter: hidePresentationToast });
await drawTriangle.initUserPage(true, context, { createParameter: hidePresentationToast });
await drawTriangle.test();
});
test('Draw line', async ({ browser, context, page }) => {
const drawLine = new DrawLine(browser, context);
await drawLine.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await drawLine.initUserPage(true, context, { customParameter: hidePresentationToast });
await drawLine.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await drawLine.initUserPage(true, context, { createParameter: hidePresentationToast });
await drawLine.test();
});
test('Draw with pencil', async ({ browser, context, page }) => {
const drawPencil = new DrawPencil(browser, context);
await drawPencil.initModPage(page, true, { customMeetingId: 'draw_pencil_meeting', customParameter: hidePresentationToast });
await drawPencil.initUserPage(true, context, { customParameter: hidePresentationToast });
await drawPencil.initModPage(page, true, { customMeetingId: 'draw_pencil_meeting', createParameter: hidePresentationToast });
await drawPencil.initUserPage(true, context, { createParameter: hidePresentationToast });
await drawPencil.test();
});
test('Type text', async ({ browser, context, page }) => {
const drawText = new DrawText(browser, context);
await drawText.initModPage(page, true, { customMeetingId: 'draw_text_meeting', customParameter: hidePresentationToast });
await drawText.initUserPage(true, context, { customParameter: hidePresentationToast });
await drawText.initModPage(page, true, { customMeetingId: 'draw_text_meeting', createParameter: hidePresentationToast });
await drawText.initUserPage(true, context, { createParameter: hidePresentationToast });
await drawText.test();
});
test('Create sticky note', async ({ browser, context, page }) => {
const drawStickyNote = new DrawStickyNote(browser, context);
await drawStickyNote.initModPage(page, true, { customMeetingId: 'draw_sticky_meeting', customParameter: hidePresentationToast });
await drawStickyNote.initUserPage(true, context, { customParameter: hidePresentationToast });
await drawStickyNote.initModPage(page, true, { customMeetingId: 'draw_sticky_meeting', createParameter: hidePresentationToast });
await drawStickyNote.initUserPage(true, context, { createParameter: hidePresentationToast });
await drawStickyNote.test();
});
test('Zoom', async ({ browser, context, page }) => {
const zoom = new Zoom(browser, context);
await zoom.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await zoom.initUserPage(true, context, { customParameter: hidePresentationToast });
await zoom.test();
});
test('Pan', async ({ browser, context, page }) => {
const pan = new Pan(browser, context);
await pan.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await pan.initUserPage(true, context, { customParameter: hidePresentationToast });
await pan.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await pan.initUserPage(true, context, { createParameter: hidePresentationToast });
await pan.test();
});
test('Eraser', async ({ browser, context, page }) => {
const eraser = new Eraser(browser, context);
await eraser.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await eraser.initUserPage(true, context, { customParameter: hidePresentationToast });
await eraser.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await eraser.initUserPage(true, context, { createParameter: hidePresentationToast });
await eraser.test();
});
test('Draw arrow', async ({ browser, context, page }) => {
const drawArrow = new DrawArrow(browser, context);
await drawArrow.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await drawArrow.initUserPage(true, context, { customParameter: hidePresentationToast });
await drawArrow.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await drawArrow.initUserPage(true, context, { createParameter: hidePresentationToast });
await drawArrow.test();
});
test('Delete drawing', async ({ browser, context, page }) => {
const deleteDrawing = new DeleteDrawing(browser, context);
await deleteDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await deleteDrawing.initUserPage(true, context, { customParameter: hidePresentationToast });
await deleteDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await deleteDrawing.initUserPage(true, context, { createParameter: hidePresentationToast });
await deleteDrawing.test();
});
test('Undo drawing', async ({ browser, context, page }) => {
const undoDrawing = new UndoDrawing(browser, context);
await undoDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await undoDrawing.initUserPage(true, context, { customParameter: hidePresentationToast });
await undoDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await undoDrawing.initUserPage(true, context, { createParameter: hidePresentationToast });
await undoDrawing.test();
});
test('Redo drawing', async ({ browser, context, page }) => {
const redoDrawing = new RedoDrawing(browser, context);
await redoDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await redoDrawing.initUserPage(true, context, { customParameter: hidePresentationToast });
await redoDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await redoDrawing.initUserPage(true, context, { createParameter: hidePresentationToast });
await redoDrawing.test();
});
test('Change color', async ({ browser, context, page }) => {
const changeColor = new ChangeStyles(browser, context);
await changeColor.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await changeColor.initUserPage(true, context, { customParameter: hidePresentationToast });
await changeColor.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await changeColor.initUserPage(true, context, { createParameter: hidePresentationToast });
await changeColor.changingColor();
});
test('Fill drawing', async ({ browser, context, page }) => {
const fillDrawing = new ChangeStyles(browser, context);
await fillDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await fillDrawing.initUserPage(true, context, { customParameter: hidePresentationToast });
await fillDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await fillDrawing.initUserPage(true, context, { createParameter: hidePresentationToast });
await fillDrawing.fillDrawing();
});
test('Dash drawing', async ({ browser, context, page }) => {
const dashDrawing = new ChangeStyles(browser, context);
await dashDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await dashDrawing.initUserPage(true, context, { customParameter: hidePresentationToast });
await dashDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await dashDrawing.initUserPage(true, context, { createParameter: hidePresentationToast });
await dashDrawing.dashDrawing();
});
test('Size drawing', async ({ browser, context, page }) => {
const sizeDrawing = new ChangeStyles(browser, context);
await sizeDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await sizeDrawing.initUserPage(true, context, { customParameter: hidePresentationToast });
await sizeDrawing.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await sizeDrawing.initUserPage(true, context, { createParameter: hidePresentationToast });
await sizeDrawing.sizeDrawing();
});
test('Real time text typing', async ({ browser, context, page }) => {
const realTimeText = new RealTimeText(browser, context);
await realTimeText.initModPage(page, true, { customMeetingId: 'draw_line_meeting', customParameter: hidePresentationToast });
await realTimeText.initUserPage(true, context, { customParameter: hidePresentationToast });
await realTimeText.initModPage(page, true, { customMeetingId: 'draw_line_meeting', createParameter: hidePresentationToast });
await realTimeText.initUserPage(true, context, { createParameter: hidePresentationToast });
await realTimeText.realTimeTextTyping();
});
test.describe.parallel('Shape Options', () => {
test('Duplicate', async ({ browser, context, page }) => {
const shapeOptions = new ShapeOptions(browser, context);
await shapeOptions.initModPage(page, true);
await shapeOptions.initUserPage(true, context);
await shapeOptions.duplicate();
});
test('Rotate', async ({ browser, context, page }) => {
const shapeOptions = new ShapeOptions(browser, context);
await shapeOptions.initModPage(page, true);
await shapeOptions.initUserPage(true, context);
await shapeOptions.rotate();
});
test('Move Shape Backward/Forward', async ({ browser, context, page }) => {
const shapeOptions = new ShapeOptions(browser, context);
await shapeOptions.initModPage(page, true);
await shapeOptions.initUserPage(true, context);
await shapeOptions.movingShape();
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -1,47 +0,0 @@
const { expect } = require('@playwright/test');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
const defaultZoomLevel = '100%';
const zoomedInZoomLevel = '125%';
const maxZoomLevel = '400%';
class Zoom extends MultiUsers {
constructor(browser, context) {
super(browser, context);
}
async test() {
await this.modPage.waitForSelector(e.resetZoomButton, ELEMENT_WAIT_LONGER_TIME);
const modWbLocator = this.modPage.getLocator(e.whiteboard);
const userWbLocator = this.userPage.getLocator(e.whiteboard);
const screenshotOptions = {
maxDiffPixels: 1000,
};
// 100%
const zoomOutButtonLocator = this.modPage.getLocator(e.zoomOutButton);
await expect(zoomOutButtonLocator).toBeDisabled();
const resetZoomButtonLocator = this.modPage.getLocator(e.resetZoomButton);
await expect(resetZoomButtonLocator).toContainText(defaultZoomLevel);
// 125%
await this.modPage.waitAndClick(e.zoomInButton);
await expect(zoomOutButtonLocator).toBeEnabled();
await expect(resetZoomButtonLocator).toContainText(zoomedInZoomLevel);
await expect(modWbLocator).toHaveScreenshot('moderator-zoom125.png', screenshotOptions);
await expect(userWbLocator).toHaveScreenshot('viewer-zoom125.png', screenshotOptions);
// max zoom
for(let i = 125; i < 400; i += 25) {
await this.modPage.waitAndClick(e.zoomInButton);
}
await expect(resetZoomButtonLocator).toContainText(maxZoomLevel);
await expect(modWbLocator).toHaveScreenshot('moderator-zoom400.png', screenshotOptions);
await expect(userWbLocator).toHaveScreenshot('viewer-zoom400.png', screenshotOptions);
}
}
exports.Zoom = Zoom;

View File

@ -39,7 +39,7 @@ You can re-enable viewing your own webcam at any point.
#### Restore downloading of original presentation
In BigBlueButton 2.4 and 2.5 we supported optional downloading of the entire presentation. In BigBlueButton 2.6 we replaced this option with the capability to download the presentation with all the annotations embedded in it. As of BigBlueButton 2.7 you are be able to do both!
In BigBlueButton 2.4 and 2.5 we supported optional downloading of the entire presentation. In BigBlueButton 2.6 we replaced this option with the capability to download the presentation with all the annotations embedded in it. As of BigBlueButton 2.7 you are be able to do both! In fact, you could select between the presentation with the current state of the annotations, the original file that was uploaded, or in case BigBlueButton had to convert the presentation file to PDF, you could also select that intermediate PDF file to be downloaded.
![You can enable original presentation downloading from the upload dialog](/img/27-enable-download-orig-presentation.png)
@ -61,6 +61,12 @@ Everyone sees the timer as it counts down.
![Everyone seeing 4 minutes timer](/img/27-timer-4mins.png)
#### Wake lock
When using BigBlueButton on a mobile device you can now enable Wake lock (if your mobile browser supports the API). When enabled, your device's screen will remain on -- i.e. will not dim -- and therefore your media connections will not be interrupted.
![Enable Wake lock from the Settings modal](/img/27-enable-wake-lock.png)
### Engagement
@ -136,6 +142,7 @@ For full details on what is new in BigBlueButton 2.7, see the release notes.
Recent releases:
- [2.7.0-rc.2](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v2.7.0-rc.2)
- [2.7.0-rc.1](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v2.7.0-rc.1)
- [2.7.0-beta.3](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v2.7.0-beta.3)
- [2.7.0-beta.2](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v2.7.0-beta.2)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 58 KiB

BIN
docs/static/img/27-enable-wake-lock.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -32,7 +32,7 @@ gem 'rb-inotify', '~> 0.10'
gem 'redis', '~> 4.1'
gem 'rubyzip', '~> 2.0'
gem 'optimist'
gem 'resque', '~> 2.5', '>= 2.5.0'
gem 'resque', '~> 2.6', '>= 2.6.0'
gem 'bbbevents', '~> 2.0', '>= 2.0.0'
gem 'rake', '>= 12.3', '<14'
gem 'tzinfo', '>= 1.2.10'

View File

@ -43,18 +43,18 @@ GEM
parser (3.1.3.0)
ast (~> 2.4.1)
racc (1.6.1)
rack (2.2.7)
rack-protection (3.0.6)
rack
rack (2.2.8)
rack-protection (3.1.0)
rack (~> 2.2, >= 2.2.4)
rainbow (3.1.1)
rake (13.0.6)
rb-inotify (0.10.1)
ffi (~> 1.0)
redis (4.8.1)
redis-namespace (1.10.0)
redis-namespace (1.11.0)
redis (>= 4)
regexp_parser (2.6.1)
resque (2.5.0)
resque (2.6.0)
mono_logger (~> 1.0)
multi_json (~> 1.0)
redis-namespace (~> 1.6)
@ -75,12 +75,12 @@ GEM
ruby-progressbar (1.11.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
sinatra (3.0.6)
sinatra (3.1.0)
mustermann (~> 3.0)
rack (~> 2.2, >= 2.2.4)
rack-protection (= 3.0.6)
rack-protection (= 3.1.0)
tilt (~> 2.0)
tilt (2.1.0)
tilt (2.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.3.0)
@ -105,7 +105,7 @@ DEPENDENCIES
rake (>= 12.3, < 14)
rb-inotify (~> 0.10)
redis (~> 4.1)
resque (~> 2.5, >= 2.5.0)
resque (~> 2.6, >= 2.6.0)
rubocop (~> 1.31.1)
rubyzip (~> 2.0)
tzinfo (>= 1.2.10)