Merge branch 'v2.6.x-release' of https://github.com/bigbluebutton/bigbluebutton into test-layout-management

This commit is contained in:
Maxim Khlobystov 2023-01-23 17:40:25 +00:00
commit efe8a152ca
122 changed files with 2061 additions and 578 deletions

View File

@ -17,7 +17,7 @@ object Dependencies {
val akkaHttpVersion = "10.2.7"
val gson = "2.8.9"
val jackson = "2.13.0"
val logback = "1.2.10"
val logback = "1.2.11"
val quicklens = "1.7.5"
val spray = "1.3.6"

View File

@ -108,16 +108,17 @@ case class EjectUserFromBreakoutInternalMsg(parentId: String, breakoutId: String
* Sent by parent meeting to breakout room to import annotated slides.
* @param userId
* @param parentMeetingId
* @param filename
* @param allPages
*/
case class CapturePresentationReqInternalMsg(userId: String, parentMeetingId: String, allPages: Boolean = true) extends InMessage
case class CapturePresentationReqInternalMsg(userId: String, parentMeetingId: String, filename: String, allPages: Boolean = true) extends InMessage
/**
* Sent by breakout room to parent meeting to obtain padId
* @param breakoutId
* @param meetingName
* @param filename
*/
case class CaptureSharedNotesReqInternalMsg(breakoutId: String, meetingName: String) extends InMessage
case class CaptureSharedNotesReqInternalMsg(breakoutId: String, filename: String) extends InMessage
// DeskShare
case class DeskShareStartedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage

View File

@ -16,8 +16,10 @@ object BreakoutModel {
assignedUsers: Vector[String],
captureNotes: Boolean,
captureSlides: Boolean,
captureNotesFilename: String,
captureSlidesFilename: String,
): BreakoutRoom2x = {
new BreakoutRoom2x(id, externalId, name, parentId, sequence, shortName, isDefaultName, freeJoin, voiceConf, assignedUsers, Vector(), Vector(), None, false, captureNotes, captureSlides)
new BreakoutRoom2x(id, externalId, name, parentId, sequence, shortName, isDefaultName, freeJoin, voiceConf, assignedUsers, Vector(), Vector(), None, false, captureNotes, captureSlides, captureNotesFilename, captureSlidesFilename)
}
}

View File

@ -40,7 +40,9 @@ trait UpdateTranscriptPubMsgHdlr {
bus.outGW.send(msgEvent)
}
if (AudioCaptions.isFloor(liveMeeting.audioCaptions, msg.header.userId)) {
val isTranscriptionEnabled = !liveMeeting.props.meetingProp.disabledFeatures.contains("liveTranscription")
if (AudioCaptions.isFloor(liveMeeting.audioCaptions, msg.header.userId) && isTranscriptionEnabled) {
val (start, end, text) = AudioCaptions.editTranscript(
liveMeeting.audioCaptions,
msg.body.transcriptId,

View File

@ -52,7 +52,10 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
val (internalId, externalId) = BreakoutRoomsUtil.createMeetingIds(liveMeeting.props.meetingProp.intId, i)
val voiceConf = BreakoutRoomsUtil.createVoiceConfId(liveMeeting.props.voiceProp.voiceConf, i)
val breakout = BreakoutModel.create(parentId, internalId, externalId, room.name, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, voiceConf, room.users, msg.body.captureNotes, msg.body.captureSlides)
val breakout = BreakoutModel.create(parentId, internalId, externalId, room.name, room.sequence, room.shortName,
room.isDefaultName, room.freeJoin, voiceConf, room.users, msg.body.captureNotes,
msg.body.captureSlides, room.captureNotesFilename, room.captureSlidesFilename)
rooms = rooms + (breakout.id -> breakout)
}
@ -73,6 +76,8 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
liveMeeting.props.breakoutProps.privateChatEnabled,
breakout.captureNotes,
breakout.captureSlides,
breakout.captureNotesFilename,
breakout.captureSlidesFilename,
)
val event = buildCreateBreakoutRoomSysCmdMsg(liveMeeting.props.meetingProp.intId, roomDetail)

View File

@ -13,13 +13,14 @@ trait EndBreakoutRoomInternalMsgHdlr extends HandlerHelpers {
def handleEndBreakoutRoomInternalMsg(msg: EndBreakoutRoomInternalMsg): Unit = {
if (liveMeeting.props.breakoutProps.captureSlides) {
val captureSlidesEvent = BigBlueButtonEvent(msg.breakoutId, CapturePresentationReqInternalMsg("system", msg.parentId))
val filename = liveMeeting.props.breakoutProps.captureSlidesFilename
val captureSlidesEvent = BigBlueButtonEvent(msg.breakoutId, CapturePresentationReqInternalMsg("system", msg.parentId, filename))
eventBus.publish(captureSlidesEvent)
}
if (liveMeeting.props.breakoutProps.captureNotes) {
val meetingName: String = liveMeeting.props.meetingProp.name
val captureNotesEvent = BigBlueButtonEvent(msg.parentId, CaptureSharedNotesReqInternalMsg(msg.breakoutId, meetingName))
val filename = liveMeeting.props.breakoutProps.captureNotesFilename
val captureNotesEvent = BigBlueButtonEvent(msg.parentId, CaptureSharedNotesReqInternalMsg(msg.breakoutId, filename))
eventBus.publish(captureNotesEvent)
}

View File

@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.presentationpod
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.api.{ CapturePresentationReqInternalMsg, CaptureSharedNotesReqInternalMsg }
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.apps.presentationpod.PresentationSender
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.LiveMeeting
@ -39,6 +40,13 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildNewPresAnnFileAvailable(fileURI: String, presId: String): NewPresAnnFileAvailableMsg = {
val header = BbbClientMsgHeader(NewPresAnnFileAvailableMsg.NAME, "not-used", "not-used")
val body = NewPresAnnFileAvailableMsgBody(fileURI, presId)
NewPresAnnFileAvailableMsg(header, body)
}
def buildBroadcastNewPresAnnFileAvailable(newPresAnnFileAvailableMsg: NewPresAnnFileAvailableMsg, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, "not-used")
val envelope = BbbCoreEnvelope(PresentationPageConvertedEventMsg.NAME, routing)
@ -49,6 +57,16 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildBroadcastPresentationConversionUpdateEvtMsg(parentMeetingId: String, status: String, presentationId: String, filename: String, temporaryPresentationId: String): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, parentMeetingId, "not-used")
val envelope = BbbCoreEnvelope(PresentationPageConvertedEventMsg.NAME, routing)
val header = BbbClientMsgHeader(PresentationConversionUpdateEvtMsg.NAME, parentMeetingId, "not-used")
val body = PresentationConversionUpdateEvtMsgBody("DEFAULT_PRESENTATION_POD", status, "not-used", presentationId, filename, temporaryPresentationId)
val event = PresentationConversionUpdateEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildBroadcastPresAnnStatusMsg(presAnnStatusMsg: PresAnnStatusMsg, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, "not-used")
val envelope = BbbCoreEnvelope(PresentationPageConvertedEventMsg.NAME, routing)
@ -116,7 +134,6 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
} else if (currentPres.isEmpty) {
log.error(s"Presentation ${presId} not found in meeting ${meetingId}")
} else {
val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
val allPages: Boolean = m.body.allPages
val pageCount = currentPres.get.pages.size
@ -128,56 +145,79 @@ trait PresentationWithAnnotationsMsgHdlr 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);
// Send Export Job to Redis
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
bus.outGW.send(job)
val annotationCount: Int = storeAnnotationPages.map(_.annotations.size).sum
// Send Annotations to Redis
val annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages)
bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting))
if (annotationCount > 0) {
// Send Export Job to Redis
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
bus.outGW.send(job)
// Send Annotations to Redis
val annotations = StoredAnnotations(jobId, presId, storeAnnotationPages)
bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting))
} else {
// Return existing uploaded file directly
val filename = currentPres.get.name
val presFilenameExt = filename.split("\\.").last
PresentationSender.broadcastSetPresentationDownloadableEvtMsg(bus, meetingId, "DEFAULT_PRESENTATION_POD", "not-used", presId, true, filename)
val fileURI = List("bigbluebutton", "presentation", "download", meetingId, s"${presId}?presFilename=${presId}.${presFilenameExt}").mkString(File.separator, File.separator, "")
val event = buildNewPresAnnFileAvailable(fileURI, presId)
handle(event, liveMeeting, bus)
}
}
}
def handle(m: CapturePresentationReqInternalMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
val parentMeetingId: String = m.parentMeetingId
val meetingId = liveMeeting.props.meetingProp.intId
val jobId = s"${meetingId}-slides" // Used as the temporaryPresentationId upon upload
val userId = m.userId
val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting()
val currentPres: Option[PresentationInPod] = presentationPods.flatMap(_.getCurrentPresentation()).headOption
val filename = m.filename
val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId)
// Informs bbb-web about the token so that when we use it to upload the presentation, it is able to look it up in the list of tokens
bus.outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, filename))
if (liveMeeting.props.meetingProp.disabledFeatures.contains("importPresentationWithAnnotationsFromBreakoutRooms")) {
log.error(s"Capturing breakout rooms slides disabled in meeting ${meetingId}.")
} else if (currentPres.isEmpty) {
log.error(s"No presentation set in meeting ${meetingId}")
bus.outGW.send(buildBroadcastPresentationConversionUpdateEvtMsg(parentMeetingId, "204", jobId, filename, presentationUploadToken))
} else {
val jobId: String = s"${meetingId}-slides" // Used as the temporaryPresentationId upon upload
val allPages: Boolean = m.allPages
val pageCount = currentPres.get.pages.size
val presId: String = PresentationPodsApp.getAllPresentationPodsInMeeting(state).flatMap(_.getCurrentPresentation.map(_.id)).mkString
val presLocation = List("var", "bigbluebutton", meetingId, meetingId, presId).mkString(File.separator, File.separator, "");
val parentMeetingId: String = m.parentMeetingId
val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres.get).get
val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num)
val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId)
val filename: String = liveMeeting.props.meetingProp.name
// Informs bbb-web about the token so that when we use it to upload the presentation, it is able to look it up in the list of tokens
bus.outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, filename))
val exportJob: ExportJob = new ExportJob(jobId, JobTypes.CAPTURE_PRESENTATION, filename, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken)
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
// Send Export Job to Redis
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
bus.outGW.send(job)
val annotationCount: Int = storeAnnotationPages.map(_.annotations.size).sum
// Send Annotations to Redis
val annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages)
bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting))
if (annotationCount > 0) {
// Send Export Job to Redis
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
bus.outGW.send(job)
// Send Annotations to Redis
val annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages)
bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting))
} else {
// Notify that no content is available to capture
bus.outGW.send(buildBroadcastPresentationConversionUpdateEvtMsg(parentMeetingId, "204", jobId, filename, presentationUploadToken))
}
}
}
@ -191,7 +231,7 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, parentMeetingId, "not-used")
val envelope = BbbCoreEnvelope(PresentationPageConversionStartedEventMsg.NAME, routing)
val header = BbbClientMsgHeader(CaptureSharedNotesReqEvtMsg.NAME, parentMeetingId, "not-used")
val body = CaptureSharedNotesReqEvtMsgBody(m.breakoutId)
val body = CaptureSharedNotesReqEvtMsgBody(m.breakoutId, m.filename)
val event = CaptureSharedNotesReqEvtMsg(header, body)
bus.outGW.send(BbbCommonEnvCoreMsg(envelope, event))

View File

@ -1,22 +1,24 @@
package org.bigbluebutton.core.domain
case class BreakoutRoom2x(
id: String,
externalId: String,
name: String,
parentId: String,
sequence: Int,
shortName: String,
isDefaultName: Boolean,
freeJoin: Boolean,
voiceConf: String,
assignedUsers: Vector[String],
users: Vector[BreakoutUser],
voiceUsers: Vector[BreakoutVoiceUser],
startedOn: Option[Long],
started: Boolean,
captureNotes: Boolean,
captureSlides: Boolean,
id: String,
externalId: String,
name: String,
parentId: String,
sequence: Int,
shortName: String,
isDefaultName: Boolean,
freeJoin: Boolean,
voiceConf: String,
assignedUsers: Vector[String],
users: Vector[BreakoutUser],
voiceUsers: Vector[BreakoutVoiceUser],
startedOn: Option[Long],
started: Boolean,
captureNotes: Boolean,
captureSlides: Boolean,
captureNotesFilename: String,
captureSlidesFilename: String
) {
}

View File

@ -45,9 +45,14 @@ trait AppsTestFixtures {
val allowModsToEjectCameras = false
val authenticatedGuest = false
val meetingLayout = ""
val captureNotesFilename = s"Room 0${sequence} (Notes)"
val captureSlidesFilename = s"Room 0${sequence} (Whiteboard)"
val metadata: collection.immutable.Map[String, String] = Map("foo" -> "bar", "bar" -> "baz", "baz" -> "foo")
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, captureNotes = false, captureSlides = false, breakoutRooms = Vector())
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence,
freeJoin = false, captureNotes = false, captureSlides = false,
breakoutRooms = Vector(), captureNotesFilename = captureNotesFilename,
captureSlidesFilename = captureSlidesFilename)
val meetingProp = MeetingProp(name = meetingName, extId = externalMeetingId, intId = meetingId,
meetingCameraCap = meetingCameraCap,

View File

@ -30,6 +30,8 @@ case class BreakoutProps(
privateChatEnabled: Boolean,
captureNotes: Boolean,
captureSlides: Boolean,
captureNotesFilename: String,
captureSlidesFilename: String,
)
case class PasswordProp(moderatorPass: String, viewerPass: String, learningDashboardAccessToken: String)

View File

@ -61,6 +61,8 @@ case class BreakoutRoomDetail(
privateChatEnabled: Boolean,
captureNotes: Boolean,
captureSlides: Boolean,
captureNotesFilename: String,
captureSlidesFilename: String,
)
/**
@ -69,7 +71,7 @@ case class BreakoutRoomDetail(
object CreateBreakoutRoomsCmdMsg { val NAME = "CreateBreakoutRoomsCmdMsg" }
case class CreateBreakoutRoomsCmdMsg(header: BbbClientMsgHeader, body: CreateBreakoutRoomsCmdMsgBody) extends StandardMsg
case class CreateBreakoutRoomsCmdMsgBody(meetingId: String, durationInMinutes: Int, record: Boolean, captureNotes: Boolean, captureSlides: Boolean, rooms: Vector[BreakoutRoomMsgBody])
case class BreakoutRoomMsgBody(name: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, users: Vector[String])
case class BreakoutRoomMsgBody(name: String, sequence: Int, shortName: String, captureNotesFilename: String, captureSlidesFilename: String, isDefaultName: Boolean, freeJoin: Boolean, users: Vector[String])
// Sent by user to request ending all the breakout rooms
object EndAllBreakoutRoomsMsg { val NAME = "EndAllBreakoutRoomsMsg" }

View File

@ -47,6 +47,6 @@ case class PresAnnStatusEvtMsgBody(presId: String, pageNumber: Int, totalPages:
object CaptureSharedNotesReqEvtMsg { val NAME = "CaptureSharedNotesReqEvtMsg" }
case class CaptureSharedNotesReqEvtMsg(header: BbbClientMsgHeader, body: CaptureSharedNotesReqEvtMsgBody) extends BbbCoreMsg
case class CaptureSharedNotesReqEvtMsgBody(breakoutId: String)
case class CaptureSharedNotesReqEvtMsgBody(breakoutId: String, filename: String)
// ------------ akka-apps to client ------------

View File

@ -44,12 +44,17 @@ trait TestFixtures {
val guestPolicy = "ALWAYS_ASK"
val authenticatedGuest = false
val metadata: collection.immutable.Map[String, String] = Map("foo" -> "bar", "bar" -> "baz", "baz" -> "foo")
val captureNotesFilename = s"Room 0${sequence} (Notes)"
val captureSlidesFilename = s"Room 0${sequence} (Whiteboard)"
val meetingProp = MeetingProp(name = meetingName, extId = externalMeetingId, intId = meetingId,
meetingCameraCap = meetingCameraCap,
maxPinnedCameras = maxPinnedCameras,
isBreakout = isBreakout.booleanValue())
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, captureNotes = false, captureSlides = false, breakoutRooms = Vector())
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, captureNotes = false,
captureSlides = false, breakoutRooms = Vector(),
endWhenNoModerator = endWhenNoModerator, endWhenNoModeratorDelayInMinutes = endWhenNoModeratorDelayInMinutes,
captureNotesFilename = captureNotesFilename, captureSlidesFilename = captureSlidesFilename)
val durationProps = DurationProps(duration = durationInMinutes, createdTime = createTime, createdDate = createDate,
meetingExpireIfNoUserJoinedInMinutes = meetingExpireIfNoUserJoinedInMinutes, meetingExpireWhenLastUserLeftInMinutes = meetingExpireWhenLastUserLeftInMinutes,

View File

@ -78,6 +78,8 @@ public class ApiParams {
public static final String BREAKOUT_ROOMS_CAPTURE_SLIDES = "breakoutRoomsCaptureSlides";
public static final String BREAKOUT_ROOMS_CAPTURE_NOTES = "breakoutRoomsCaptureNotes";
public static final String BREAKOUT_ROOMS_CAPTURE_SLIDES_FILENAME = "breakoutRoomsCaptureSlidesFilename";
public static final String BREAKOUT_ROOMS_CAPTURE_NOTES_FILENAME = "breakoutRoomsCaptureNotesFilename";
public static final String BREAKOUT_ROOMS_ENABLED = "breakoutRoomsEnabled";
public static final String BREAKOUT_ROOMS_RECORD = "breakoutRoomsRecord";
public static final String BREAKOUT_ROOMS_PRIVATE_CHAT_ENABLED = "breakoutRoomsPrivateChatEnabled";

View File

@ -547,26 +547,29 @@ public class MeetingService implements MessageListener {
return recordingService.isRecordingExist(recordId);
}
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, String page, String size) {
int p;
int s;
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, String offset, String limit) {
Pageable pageable = null;
int o = -1;
int l = -1;
try {
p = Integer.parseInt(page);
o = Integer.parseInt(offset);
if(o < 0) o = 0;
} catch(NumberFormatException e) {
p = 0;
log.info("Invalid offset parameter {}", offset);
o = 0;
}
try {
s = Integer.parseInt(size);
l = Integer.parseInt(limit);
if(l < 1) l = 1;
else if(l > 100) l = 100;
} catch(NumberFormatException e) {
s = 25;
log.info("Invalid limit parameter {}", limit);
}
log.info("{} {}", p, s);
Pageable pageable = PageRequest.of(p, s);
return recordingService.getRecordings2x(idList, states, metadataFilters, pageable);
if(l != -1) pageable = PageRequest.ofSize(l);
return recordingService.getRecordings2x(idList, states, metadataFilters, o, pageable);
}
public boolean existsAnyRecording(List<String> idList) {
@ -633,6 +636,8 @@ public class MeetingService implements MessageListener {
params.put(ApiParams.FREE_JOIN, message.freeJoin.toString());
params.put(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES, message.captureSlides.toString());
params.put(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES, message.captureNotes.toString());
params.put(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES_FILENAME, message.captureNotesFilename.toString());
params.put(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES_FILENAME, message.captureSlidesFilename.toString());
params.put(ApiParams.ATTENDEE_PW, message.viewerPassword);
params.put(ApiParams.MODERATOR_PW, message.moderatorPassword);
params.put(ApiParams.DIAL_NUMBER, message.dialNumber);

View File

@ -112,6 +112,8 @@ public class ParamsProcessorUtil {
private boolean defaultBreakoutRoomsRecord;
private boolean defaultBreakoutRoomsCaptureSlides = false;
private boolean defaultBreakoutRoomsCaptureNotes = false;
private String defaultBreakoutRoomsCaptureSlidesFilename = CONF_NAME;
private String defaultBreakoutRoomsCaptureNotesFilename = CONF_NAME;
private boolean defaultbreakoutRoomsPrivateChatEnabled;
private boolean defaultLockSettingsDisableCam;
@ -295,7 +297,19 @@ public class ParamsProcessorUtil {
breakoutRoomsCaptureNotes = Boolean.parseBoolean(breakoutRoomsCaptureNotesParam);
}
return new BreakoutRoomsParams(breakoutRoomsRecord, breakoutRoomsPrivateChatEnabled, breakoutRoomsCaptureNotes, breakoutRoomsCaptureSlides);
String breakoutRoomsCaptureNotesFilename = defaultBreakoutRoomsCaptureNotesFilename;
String breakoutRoomsCaptureNotesFilenameParam = params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES_FILENAME);
if (!StringUtils.isEmpty(breakoutRoomsCaptureNotesFilenameParam)) {
breakoutRoomsCaptureNotesFilename = breakoutRoomsCaptureNotesFilenameParam;
}
String breakoutRoomsCaptureSlidesFilename = defaultBreakoutRoomsCaptureSlidesFilename;
String breakoutRoomsCaptureSlidesFilenameParam = params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES_FILENAME);
if (!StringUtils.isEmpty(breakoutRoomsCaptureSlidesFilenameParam)) {
breakoutRoomsCaptureSlidesFilename = breakoutRoomsCaptureSlidesFilenameParam;
}
return new BreakoutRoomsParams(breakoutRoomsRecord, breakoutRoomsPrivateChatEnabled, breakoutRoomsCaptureNotes, breakoutRoomsCaptureSlides, breakoutRoomsCaptureNotesFilename, breakoutRoomsCaptureSlidesFilename);
}
private LockSettingsParams processLockSettingsParams(Map<String, String> params) {
@ -769,6 +783,8 @@ public class ParamsProcessorUtil {
meeting.setFreeJoin(Boolean.parseBoolean(params.get(ApiParams.FREE_JOIN)));
meeting.setCaptureSlides(Boolean.parseBoolean(params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES)));
meeting.setCaptureNotes(Boolean.parseBoolean(params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES)));
meeting.setCaptureNotesFilename(params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES_FILENAME));
meeting.setCaptureSlidesFilename(params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES_FILENAME));
meeting.setParentMeetingId(parentMeetingId);
}

View File

@ -37,7 +37,7 @@ public interface RecordingService {
String getCaptionTrackInboxDir();
String getCaptionsDir();
boolean isRecordingExist(String recordId);
String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, Pageable pageable);
String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, int offset, Pageable pageable);
boolean existAnyRecording(List<String> idList);
boolean changeState(String recordingId, String state);
void updateMetaParams(List<String> recordIDs, Map<String,String> metaParams);
@ -47,9 +47,9 @@ public interface RecordingService {
void processMakePresentationDownloadableMsg(MakePresentationDownloadableMsg msg);
File getDownloadablePresentationFile(String meetingId, String presId, String presFilename);
default <T> Page<T> listToPage(List<T> list, Pageable pageable) {
int start = (int) pageable.getOffset();
int end = (int) (Math.min((start + pageable.getPageSize()), list.size()));
return new PageImpl<>(list.subList(start, end), pageable, list.size());
// Construct page using offset and limit parameters
default <T> Page<T> listToPage(List<T> list, int offset, Pageable pageable) {
int end = (int) (Math.min((offset + pageable.getPageSize()), list.size()));
return new PageImpl<>(list.subList(offset, end), pageable, list.size());
}
}

View File

@ -119,14 +119,8 @@ public final class Util {
boolean downloadable
) throws IOException {
File downloadMarker = Util.getPresFileDownloadMarker(presFileDir, presId);
if (downloadable) {
if (downloadMarker != null && ! downloadMarker.exists()) {
downloadMarker.createNewFile();
}
} else {
if (downloadMarker != null && downloadMarker.exists()) {
downloadMarker.delete();
}
if (downloadable && downloadMarker != null && ! downloadMarker.exists()) {
downloadMarker.createNewFile();
}
}
}

View File

@ -5,11 +5,15 @@ public class BreakoutRoomsParams {
public final Boolean privateChatEnabled;
public final Boolean captureNotes;
public final Boolean captureSlides;
public final String captureNotesFilename;
public final String captureSlidesFilename;
public BreakoutRoomsParams(Boolean record, Boolean privateChatEnabled, Boolean captureNotes, Boolean captureSlides) {
public BreakoutRoomsParams(Boolean record, Boolean privateChatEnabled, Boolean captureNotes, Boolean captureSlides, String captureNotesFilename, String captureSlidesFilename) {
this.record = record;
this.privateChatEnabled = privateChatEnabled;
this.captureNotes = captureNotes;
this.captureSlides = captureSlides;
this.captureNotesFilename = captureNotesFilename;
this.captureSlidesFilename = captureSlidesFilename;
}
}

View File

@ -44,6 +44,8 @@ public class Meeting {
private Boolean freeJoin = false;
private Boolean captureSlides = false;
private Boolean captureNotes = false;
private String captureSlidesFilename = "bbb-none";
private String captureNotesFilename = "bbb-none";
private Integer duration = 0;
private long createdTime = 0;
private long startTime = 0;
@ -328,6 +330,14 @@ public class Meeting {
this.captureNotes = captureNotes;
}
public void setCaptureNotesFilename(String filename) {
this.captureNotesFilename = filename;
}
public void setCaptureSlidesFilename(String filename) {
this.captureSlidesFilename = filename;
}
public Integer getDuration() {
return duration;
}

View File

@ -21,6 +21,8 @@ public class CreateBreakoutRoom implements IMessage {
public final Boolean privateChatEnabled;
public final Boolean captureNotes; // Upload shared notes to main room after breakout room end
public final Boolean captureSlides; // Upload annotated breakout slides to main room after breakout room end
public final String captureNotesFilename;
public final String captureSlidesFilename;
public CreateBreakoutRoom(String meetingId,
String parentMeetingId,
@ -39,7 +41,9 @@ public class CreateBreakoutRoom implements IMessage {
Boolean record,
Boolean privateChatEnabled,
Boolean captureNotes,
Boolean captureSlides) {
Boolean captureSlides,
String captureNotesFilename,
String captureSlidesFilename) {
this.meetingId = meetingId;
this.parentMeetingId = parentMeetingId;
this.name = name;
@ -58,5 +62,7 @@ public class CreateBreakoutRoom implements IMessage {
this.privateChatEnabled = privateChatEnabled;
this.captureNotes = captureNotes;
this.captureSlides = captureSlides;
this.captureNotesFilename = captureNotesFilename;
this.captureSlidesFilename = captureSlidesFilename;
}
}

View File

@ -14,6 +14,7 @@ public interface XmlService {
String thumbnailToXml(Thumbnail thumbnail);
String callbackDataToXml(CallbackData callbackData);
String constructResponseFromRecordingsXml(String xml);
String constructPaginatedResponse(Page<?> page, String response);
String constructPaginatedResponse(Page<?> page, int offset, String response);
Recording xmlToRecording(String recordId, String xml);
String noRecordings();
}

View File

@ -72,65 +72,87 @@ public class RecordingServiceDbImpl implements RecordingService {
}
@Override
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, Pageable pageable) {
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, int offset, Pageable pageable) {
// If no IDs or limit were provided return no recordings instead of every recording
if((idList == null || idList.isEmpty()) && pageable == null) return xmlService.noRecordings();
logger.info("Retrieving all recordings");
Set<Recording> recordings = new HashSet<>();
recordings.addAll(dataStore.findAll(Recording.class));
Set<Recording> recordings = new HashSet<>(dataStore.findAll(Recording.class));
logger.info("{} recordings found", recordings.size());
Set<Recording> recordingsById = new HashSet<>();
for(String id: idList) {
List<Recording> r = dataStore.findRecordingsByMeetingId(id);
if(idList != null && !idList.isEmpty()) {
Set<Recording> recordingsById = new HashSet<>();
if(r == null || r.size() == 0) {
Recording recording = dataStore.findRecordingByRecordId(id);
if(recording != null) {
r = new ArrayList<>();
r.add(recording);
for(String id: idList) {
logger.info("Finding recordings using meeting ID with value {}", id);
List<Recording> recordingsByMeetingId = dataStore.findRecordingsByMeetingId(id);
if(recordingsByMeetingId == null || recordingsByMeetingId.isEmpty()) {
logger.info("Finding recordings using recording ID with value {}", id);
Recording recording = dataStore.findRecordingByRecordId(id);
if(recording != null) {
logger.info("Recording found");
recordingsById.add(recording);
}
} else {
logger.info("{} recordings found", recordingsByMeetingId.size());
recordingsById.addAll(recordingsByMeetingId);
}
}
if(r != null) recordingsById.addAll(r);
}
logger.info("Filtering recordings by meeting ID");
if(recordingsById.size() > 0) {
logger.info("Filtering recordings by ID");
recordings.retainAll(recordingsById);
}
logger.info("{} recordings remaining", recordings.size());
Set<Recording> recordingsByState = new HashSet<>();
for(String state: states) {
List<Recording> r = dataStore.findRecordingsByState(state);
if(r != null) recordingsByState.addAll(r);
logger.info("{} recordings remain", recordings.size());
}
logger.info("Filtering recordings by state");
if(recordingsByState.size() > 0) {
if(states != null && !states.isEmpty()) {
Set<Recording> recordingsByState = new HashSet<>();
for(String state: states) {
logger.info("Finding recordings by state {}", state);
List<Recording> r = dataStore.findRecordingsByState(state);
if(state != null && !state.isEmpty()) {
logger.info("{} recordings found", r.size());
recordingsByState.addAll(r);
}
}
logger.info("Filtering recordings by state");
recordings.retainAll(recordingsByState);
}
logger.info("{} recordings remaining", recordings.size());
List<Metadata> metadata = new ArrayList<>();
for(Map.Entry<String, String> metadataFilter: metadataFilters.entrySet()) {
List<Metadata> m = dataStore.findMetadataByFilter(metadataFilter.getKey(), metadataFilter.getValue());
if(m != null) metadata.addAll(m);
logger.info("{} recordings remain", recordings.size());
}
Set<Recording> recordingsByMetadata = new HashSet<>();
for(Metadata m: metadata) {
recordingsByMetadata.add(m.getRecording());
}
if(metadataFilters != null && !metadataFilters.isEmpty()) {
List<Metadata> metadata = new ArrayList<>();
logger.info("Filtering recordings by metadata");
if(recordingsByMetadata.size() > 0) {
for(Map.Entry<String, String> filter: metadataFilters.entrySet()) {
logger.info("Finding metadata using filter {} {}", filter.getKey(), filter.getValue());
List<Metadata> metadataByFilter = dataStore.findMetadataByFilter(filter.getKey(), filter.getValue());
if(metadataByFilter != null) {
logger.info("{} metadata found", metadataByFilter.size());
metadata.addAll(metadataByFilter);
}
}
Set<Recording> recordingsByMetadata = new HashSet<>();
for(Metadata m: metadata) recordingsByMetadata.add(m.getRecording());
logger.info("Filtering recordings by metadata");
recordings.retainAll(recordingsByMetadata);
logger.info("{} recordings remain", recordings.size());
}
logger.info("{} recordings remaining", recordings.size());
Page<Recording> recordingsPage = listToPage(new ArrayList<>(recordings), pageable);
// If no/invalid pagination parameters were given do not paginate the response
if(pageable == null) {
String recordingsXml = xmlService.recordingsToXml(recordings);
return xmlService.constructResponseFromRecordingsXml(recordingsXml);
}
Page<Recording> recordingsPage = listToPage(new ArrayList<>(recordings), offset, pageable);
String recordingsXml = xmlService.recordingsToXml(recordingsPage.getContent());
String response = xmlService.constructResponseFromRecordingsXml(recordingsXml);
return xmlService.constructPaginatedResponse(recordingsPage, response);
return xmlService.constructPaginatedResponse(recordingsPage, offset, response);
}
@Override

View File

@ -201,12 +201,19 @@ public class RecordingServiceFileImpl implements RecordingService {
return recordingServiceHelper.putRecordingTextTrack(track);
}
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, Pageable pageable) {
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, int offset, Pageable pageable) {
// If no IDs or limit were provided return no recordings instead of every recording
if(idList.isEmpty() && pageable == null) return xmlService.noRecordings();
List<RecordingMetadata> recsList = getRecordingsMetadata(idList, states);
ArrayList<RecordingMetadata> recs = filterRecordingsByMetadata(recsList, metadataFilters);
Page<RecordingMetadata> recordingsPage = listToPage(recs, pageable);
String response = recordingServiceHelper.getRecordings2x(recs);
return xmlService.constructPaginatedResponse(recordingsPage, response);
// If no/invalid pagination parameters were given do not paginate the response
if(pageable == null) return recordingServiceHelper.getRecordings2x(recs);
Page<RecordingMetadata> recordingsPage = listToPage(recs, offset, pageable);
String response = recordingServiceHelper.getRecordings2x(new ArrayList<RecordingMetadata>(recordingsPage.getContent()));
return xmlService.constructPaginatedResponse(recordingsPage, offset, response);
}
private RecordingMetadata getRecordingMetadata(File dir) {

View File

@ -72,7 +72,7 @@ public class XmlServiceImpl implements XmlService {
@Override
public String recordingToXml(Recording recording) {
logger.info("Converting {} to xml", recording);
// logger.info("Converting {} to xml", recording);
try {
setup();
Document document = builder.newDocument();
@ -125,7 +125,7 @@ public class XmlServiceImpl implements XmlService {
@Override
public String metadataToXml(Metadata metadata) {
logger.info("Converting {} to xml", metadata);
// logger.info("Converting {} to xml", metadata);
try {
setup();
@ -148,7 +148,7 @@ public class XmlServiceImpl implements XmlService {
@Override
public String playbackFormatToXml(PlaybackFormat playbackFormat) {
logger.info("Converting {} to xml", playbackFormat);
// logger.info("Converting {} to xml", playbackFormat);
try {
setup();
@ -187,7 +187,7 @@ public class XmlServiceImpl implements XmlService {
@Override
public String thumbnailToXml(Thumbnail thumbnail) {
logger.info("Converting {} to xml", thumbnail);
// logger.info("Converting {} to xml", thumbnail);
try {
setup();
@ -211,7 +211,7 @@ public class XmlServiceImpl implements XmlService {
@Override
public String callbackDataToXml(CallbackData callbackData) {
logger.info("Converting {} to xml", callbackData);
// logger.info("Converting {} to xml", callbackData);
try {
setup();
@ -264,7 +264,39 @@ public class XmlServiceImpl implements XmlService {
}
@Override
public String constructPaginatedResponse(Page<?> page, String response) {
public String noRecordings() {
logger.info("Constructing no recordings response");
try {
setup();
Document document = builder.newDocument();
Element rootElement = createElement(document, "response", null);
document.appendChild(rootElement);
Element returnCode = createElement(document, "returncode", "SUCCESS");
rootElement.appendChild(returnCode);
Element messageKey = createElement(document, "messageKey", "noRecordings");
rootElement.appendChild(messageKey);
Element message = createElement(document, "message", "No recordings found. This may occur if you attempt to retrieve all recordings.");
rootElement.appendChild(message);
String result = documentToString(document);
// logger.info("========== Result ==========");
// logger.info("{}", result);
// logger.info("============================");
return result;
} catch(Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public String constructPaginatedResponse(Page<?> page, int offset, String response) {
logger.info("Constructing paginated response");
try {
@ -283,7 +315,7 @@ public class XmlServiceImpl implements XmlService {
Document secondDoc;
Node node;
xml = pageableToXml(page.getPageable());
xml = pageableToXml(page.getPageable(), offset);
secondDoc = builder.parse(new ByteArrayInputStream(xml.getBytes()));
node = document.importNode(secondDoc.getDocumentElement(), true);
pagination.appendChild(node);
@ -291,14 +323,14 @@ public class XmlServiceImpl implements XmlService {
Element totalElements = createElement(document, "totalElements", String.valueOf(page.getTotalElements()));
pagination.appendChild(totalElements);
Element last = createElement(document, "last", String.valueOf(page.isLast()));
pagination.appendChild(last);
// Element last = createElement(document, "last", String.valueOf(page.isLast()));
// pagination.appendChild(last);
Element totalPages = createElement(document, "totalPages", String.valueOf(page.getTotalPages()));
pagination.appendChild(totalPages);
// Element totalPages = createElement(document, "totalPages", String.valueOf(page.getTotalPages()));
// pagination.appendChild(totalPages);
Element first = createElement(document, "first", String.valueOf(page.isFirst()));
pagination.appendChild(first);
// Element first = createElement(document, "first", String.valueOf(page.isFirst()));
// pagination.appendChild(first);
Element empty = createElement(document, "empty", String.valueOf(!page.hasContent()));
pagination.appendChild(empty);
@ -317,7 +349,7 @@ public class XmlServiceImpl implements XmlService {
return null;
}
private String pageableToXml(Pageable pageable) {
private String pageableToXml(Pageable pageable, int o) {
logger.info("Converting {} to xml", pageable);
try {
@ -327,28 +359,28 @@ public class XmlServiceImpl implements XmlService {
Element rootElement = createElement(document, "pageable", null);
document.appendChild(rootElement);
Sort sort = pageable.getSort();
Element sortElement = createElement(document, "sort", null);
// Sort sort = pageable.getSort();
// Element sortElement = createElement(document, "sort", null);
//
// Element unsorted = createElement(document, "unsorted", String.valueOf(sort.isUnsorted()));
// sortElement.appendChild(unsorted);
//
// Element sorted = createElement(document, "sorted", String.valueOf(sort.isSorted()));
// sortElement.appendChild(sorted);
//
// Element empty = createElement(document, "empty", String.valueOf(sort.isEmpty()));
// sortElement.appendChild(empty);
//
// rootElement.appendChild(sortElement);
Element unsorted = createElement(document, "unsorted", String.valueOf(sort.isUnsorted()));
sortElement.appendChild(unsorted);
Element sorted = createElement(document, "sorted", String.valueOf(sort.isSorted()));
sortElement.appendChild(sorted);
Element empty = createElement(document, "empty", String.valueOf(sort.isEmpty()));
sortElement.appendChild(empty);
rootElement.appendChild(sortElement);
Element offset = createElement(document, "offset", String.valueOf(pageable.getOffset()));
Element offset = createElement(document, "offset", String.valueOf(o));
rootElement.appendChild(offset);
Element pageSize = createElement(document, "pageSize", String.valueOf(pageable.getPageSize()));
rootElement.appendChild(pageSize);
Element limit = createElement(document, "limit", String.valueOf(pageable.getPageSize()));
rootElement.appendChild(limit);
Element pageNumber = createElement(document, "pageNumber", String.valueOf(pageable.getPageNumber()));
rootElement.appendChild(pageNumber);
// Element pageNumber = createElement(document, "pageNumber", String.valueOf(pageable.getPageNumber()));
// rootElement.appendChild(pageNumber);
Element paged = createElement(document, "paged", String.valueOf(pageable.isPaged()));
rootElement.appendChild(paged);

View File

@ -193,6 +193,8 @@ class BbbWebApiGWApp(
privateChatEnabled = breakoutParams.privateChatEnabled.booleanValue(),
captureNotes = breakoutParams.captureNotes.booleanValue(),
captureSlides = breakoutParams.captureSlides.booleanValue(),
captureNotesFilename = breakoutParams.captureNotesFilename,
captureSlidesFilename = breakoutParams.captureSlidesFilename,
)
val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg,

View File

@ -12,7 +12,7 @@ case class CreateBreakoutRoomMsg(meetingId: String, parentMeetingId: String,
name: String, sequence: Integer, freeJoin: Boolean, dialNumber: String,
voiceConfId: String, viewerPassword: String, moderatorPassword: String, duration: Int,
sourcePresentationId: String, sourcePresentationSlide: Int,
record: Boolean, captureNotes: Boolean, captureSlides: Boolean) extends ApiMsg
record: Boolean, captureNotes: Boolean, captureSlides: Boolean, captureNotesFilename: String, captureSlidesFilename: String) extends ApiMsg
case class AddUserSession(token: String, session: UserSession)
case class RegisterUser(meetingId: String, intUserId: String, name: String, role: String,

View File

@ -105,6 +105,8 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
msg.body.room.privateChatEnabled,
msg.body.room.captureNotes,
msg.body.room.captureSlides,
msg.body.room.captureNotesFilename,
msg.body.room.captureSlidesFilename,
))
}

View File

@ -7400,6 +7400,19 @@
"version": "1.0.0",
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"license": "MIT"
@ -10278,8 +10291,9 @@
"license": "MIT"
},
"node_modules/json5": {
"version": "2.2.1",
"license": "MIT",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": {
"json5": "lib/cli.js"
},
@ -14563,8 +14577,9 @@
}
},
"node_modules/tsconfig-paths/node_modules/json5": {
"version": "1.0.1",
"license": "MIT",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": {
"minimist": "^1.2.0"
},
@ -19960,6 +19975,12 @@
"fs.realpath": {
"version": "1.0.0"
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"optional": true
},
"function-bind": {
"version": "1.1.1"
},
@ -21692,7 +21713,9 @@
"version": "1.0.1"
},
"json5": {
"version": "2.2.1"
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
},
"jsonfile": {
"version": "6.1.0",
@ -24130,7 +24153,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": {
"minimist": "^1.2.0"
}

View File

@ -205,10 +205,10 @@ const UserDatailsComponent = (props) => {
);
return (
<div className="p-6 flex flex-row justify-between items-center">
<div className="min-w-[40%] text-ellipsis">{question}</div>
<tr className="p-6 flex flex-row justify-between items-center">
<td className="min-w-[40%] text-ellipsis">{question}</td>
{ isAnonymous ? (
<div
<td
className="min-w-[20%] grow text-center mx-3"
>
<span
@ -232,11 +232,11 @@ const UserDatailsComponent = (props) => {
/>
</svg>
</span>
</div>
</td>
) : (
<div className="min-w-[20%] grow text-center mx-3">{answers.map((answer) => <p title={answer} className="overflow-hidden text-ellipsis">{answer}</p>)}</div>
<td className="min-w-[20%] grow text-center mx-3">{answers.map((answer) => <p title={answer} className="overflow-hidden text-ellipsis">{answer}</p>)}</td>
) }
<div
<td
className="min-w-[40%] text-ellipsis text-center overflow-hidden"
title={mostCommonAnswer
? capitalizeFirstLetter(mostCommonAnswer)
@ -248,8 +248,8 @@ const UserDatailsComponent = (props) => {
id: 'app.learningDashboard.usersTable.notAvailable',
defaultMessage: 'N/A',
}) }
</div>
</div>
</td>
</tr>
);
}
@ -257,14 +257,14 @@ const UserDatailsComponent = (props) => {
category, average, activityPoints, totalOfActivity,
) {
return (
<div className="p-6 flex flex-row justify-between items-end">
<div className="min-w-[20%] text-ellipsis overflow-hidden">
<tr className="p-6 flex flex-row justify-between items-end">
<td className="min-w-[20%] text-ellipsis overflow-hidden">
<FormattedMessage
id={`app.learningDashboard.userDetails.${toCamelCase(category)}`}
defaultMessage={category}
/>
</div>
<div className="min-w-[60%] grow text-center text-sm">
</td>
<td className="min-w-[60%] grow text-center text-sm">
<div className="mb-2">
{ (function getAverage() {
if (average >= 0 && category === 'Talk Time') return tsToHHmmss(average);
@ -284,13 +284,13 @@ const UserDatailsComponent = (props) => {
: null }
</div>
</div>
</div>
<div className="min-w-[20%] text-sm text-ellipsis overflow-hidden text-right rtl:text-left">
</td>
<td className="min-w-[20%] text-sm text-ellipsis overflow-hidden text-right rtl:text-left">
{ activityPoints >= 0
? <FormattedNumber value={activityPoints} minimumFractionDigits="0" maximumFractionDigits="1" />
: <FormattedMessage id="app.learningDashboard.usersTable.notAvailable" defaultMessage="N/A" /> }
</div>
</div>
</td>
</tr>
);
}
@ -332,7 +332,7 @@ const UserDatailsComponent = (props) => {
aria-hidden="true"
/>
</div>
<p className="break-words text-center">{user.name}</p>
<h3 className="break-words text-center">{user.name}</h3>
</div>
<div className="bg-white shadow rounded mb-4">
<div className="p-6 text-lg flex items-center">
@ -341,7 +341,7 @@ const UserDatailsComponent = (props) => {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="ltr:ml-2 rtl:mr-2"><FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" /></p>
<h3 className="ltr:ml-2 rtl:mr-2"><FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" /></h3>
</div>
<div className="p-6 m-px bg-gray-100">
<div className="h-6 relative before:bg-gray-500 before:absolute before:w-[10px] before:h-[10px] before:rounded-full before:left-0 before:top-[calc(50%-5px)] after:bg-gray-500 after:absolute after:w-[10px] after:h-[10px] after:rounded-full after:right-0 after:top-[calc(50%-5px)]">
@ -420,7 +420,7 @@ const UserDatailsComponent = (props) => {
</div>
{ !user.isModerator && (
<>
<div className="bg-white shadow rounded mb-4 table w-full">
<div className="bg-white shadow rounded mb-4 w-full">
<div className="p-6 text-lg flex items-center">
<div className="p-2 rounded-full bg-green-100 text-green-700">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -428,48 +428,50 @@ const UserDatailsComponent = (props) => {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
</svg>
</div>
<p className="ltr:ml-2 rtl:mr-2">
<h3 className="ltr:ml-2 rtl:mr-2">
<FormattedMessage id="app.learningDashboard.indicators.activityScore" defaultMessage="Activity Score" />
:&nbsp;
<span className="font-bold">
<FormattedNumber value={getActivityScore(user, users, totalPolls)} minimumFractionDigits="0" maximumFractionDigits="1" />
</span>
</p>
</h3>
</div>
<div className="p-6 py-2 m-px bg-gray-200 flex flex-row justify-between text-xs text-gray-700">
<div className="min-w-[20%] text-ellipsis"><FormattedMessage id="app.learningDashboard.userDetails.category" defaultMessage="Category" /></div>
<div className="grow text-center"><FormattedMessage id="app.learningDashboard.userDetails.average" defaultMessage="Average" /></div>
<div className="min-w-[20%] text-ellipsis text-right rtl:text-left"><FormattedMessage id="app.learningDashboard.userDetails.activityPoints" defaultMessage="Activity Points" /></div>
</div>
{ ['Talk Time', 'Messages', 'Emojis', 'Raise Hands', 'Poll Votes'].map((category) => {
let totalOfActivity = 0;
<table className="bg-white shadow rounded mb-4 table w-full">
<tr className="p-6 py-2 m-px bg-gray-200 flex flex-row justify-between text-xs text-gray-700">
<th aria-label="Category" className="min-w-[20%] text-ellipsis font-normal text-left"><FormattedMessage id="app.learningDashboard.userDetails.category" defaultMessage="Category" /></th>
<th aria-label="Average" className="grow text-center font-normal"><FormattedMessage id="app.learningDashboard.userDetails.average" defaultMessage="Average" /></th>
<th aria-label="Activity Points" className="min-w-[20%] text-ellipsis text-right rtl:text-left font-normal"><FormattedMessage id="app.learningDashboard.userDetails.activityPoints" defaultMessage="Activity Points" /></th>
</tr>
{ ['Talk Time', 'Messages', 'Emojis', 'Raise Hands', 'Poll Votes'].map((category) => {
let totalOfActivity = 0;
switch (category) {
case 'Talk Time':
totalOfActivity = user.talk.totalTime;
break;
case 'Messages':
totalOfActivity = user.totalOfMessages;
break;
case 'Emojis':
totalOfActivity = user.emojis.filter((emoji) => emoji.name !== 'raiseHand').length;
break;
case 'Raise Hands':
totalOfActivity = user.emojis.filter((emoji) => emoji.name === 'raiseHand').length;
break;
case 'Poll Votes':
totalOfActivity = Object.values(user.answers).length;
break;
default:
}
switch (category) {
case 'Talk Time':
totalOfActivity = user.talk.totalTime;
break;
case 'Messages':
totalOfActivity = user.totalOfMessages;
break;
case 'Emojis':
totalOfActivity = user.emojis.filter((emoji) => emoji.name !== 'raiseHand').length;
break;
case 'Raise Hands':
totalOfActivity = user.emojis.filter((emoji) => emoji.name === 'raiseHand').length;
break;
case 'Poll Votes':
totalOfActivity = Object.values(user.answers).length;
break;
default:
}
return renderActivityScoreItem(
category,
averages[category],
activityPointsFunctions[category](user),
totalOfActivity,
);
}) }
return renderActivityScoreItem(
category,
averages[category],
activityPointsFunctions[category](user),
totalOfActivity,
);
}) }
</table>
</div>
<div className="bg-white shadow rounded">
<div className="p-6 text-lg flex items-center">
@ -478,18 +480,20 @@ const UserDatailsComponent = (props) => {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
</div>
<p className="ltr:ml-2 rtl:mr-2"><FormattedMessage id="app.learningDashboard.indicators.polls" defaultMessage="Polls" /></p>
<h3 className="ltr:ml-2 rtl:mr-2"><FormattedMessage id="app.learningDashboard.indicators.polls" defaultMessage="Polls" /></h3>
</div>
<div className="p-6 py-2 m-px bg-gray-200 flex flex-row justify-between text-xs text-gray-700">
<div className="min-w-[40%] text-ellipsis"><FormattedMessage id="app.learningDashboard.userDetails.poll" defaultMessage="Poll" /></div>
<div className="grow text-center"><FormattedMessage id="app.learningDashboard.userDetails.response" defaultMessage="Response" /></div>
<div className="min-w-[40%] text-ellipsis text-center"><FormattedMessage id="app.learningDashboard.userDetails.mostCommonAnswer" defaultMessage="Most Common Answer" /></div>
</div>
{ Object.values(polls || {})
.map((poll) => renderPollItem(
poll,
getUserAnswer(poll),
)) }
<table className="w-full">
<tr className="p-6 py-2 m-px bg-gray-200 flex flex-row justify-between text-xs text-gray-700">
<th aria-label="Poll" className="min-w-[40%] text-ellipsis font-normal text-left"><FormattedMessage id="app.learningDashboard.userDetails.poll" defaultMessage="Poll" /></th>
<th aria-label="Response" className="grow text-center font-normal"><FormattedMessage id="app.learningDashboard.userDetails.response" defaultMessage="Response" /></th>
<th aria-label="Most Common Answer" className="min-w-[40%] text-ellipsis text-center font-normal"><FormattedMessage id="app.learningDashboard.userDetails.mostCommonAnswer" defaultMessage="Most Common Answer" /></th>
</tr>
{ Object.values(polls || {})
.map((poll) => renderPollItem(
poll,
getUserAnswer(poll),
)) }
</table>
</div>
</>
) }
@ -500,7 +504,6 @@ const UserDatailsComponent = (props) => {
const UserDetailsContainer = (props) => {
const { isOpen, dispatch, user } = useContext(UserDetailsContext);
return ReactDOM.createPortal(
<UserDatailsComponent
{...{

View File

@ -1,11 +1,13 @@
#!/bin/bash
while getopts i:r:s:m: flag
while getopts i:r:s:m:o:l: flag
do
case "${flag}" in
i) MEETING_ID=${OPTARG};;
r) RECORD_ID=${OPTARG};;
s) STATE=${OPTARG};;
m) META=${OPTARG};;
o) OFFSET=${OPTARG};;
l) LIMIT=${OPTARG};;
esac
done
@ -17,7 +19,9 @@ QUERY=""
if ! [[ -z ${MEETING_ID+x} ]]; then QUERY+="meetingID=$MEETING_ID&"; fi
if ! [[ -z ${RECORD_ID+x} ]]; then QUERY+="recordID=$RECORD_ID&"; fi
if ! [[ -z ${STATE+x} ]]; then QUERY+="state=$STATE&"; fi
if ! [[ -z ${META+x} ]]; then QUERY+="meta=$META"; fi
if ! [[ -z ${META+x} ]]; then QUERY+="meta=$META&"; fi
if ! [[ -z ${OFFSET+x} ]]; then QUERY+="offset=$OFFSET&"; fi
if ! [[ -z ${LIMIT+x} ]]; then QUERY+="limit=$LIMIT"; fi
echo "query: $QUERY"
@ -26,7 +30,7 @@ if [ "${QUERY:$INDEX:1}" = "&" ]; then QUERY=${QUERY:0:$INDEX}; fi
echo "query: $QUERY"
SALT=
SALT=""
DATA="$ENDPOINT$QUERY$SALT"
echo "data: $DATA"

View File

@ -75,7 +75,7 @@
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
<version>1.2.11</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
@ -85,7 +85,7 @@
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<version>1.2.11</version>
</dependency>
<dependency>

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.6.0-beta.5
BIGBLUEBUTTON_RELEASE=2.6.0-beta.7

View File

@ -388,7 +388,7 @@ start_bigbluebutton () {
}
fi
systemctl start bigbluebutton.target
systemctl restart bigbluebutton.target
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
systemctl start mongod
@ -1686,7 +1686,8 @@ if [ -n "$HOST" ]; then
echo "Restarting BigBlueButton $BIGBLUEBUTTON_RELEASE ..."
systemctl restart bigbluebutton.target
stop_bigbluebutton
start_bigbluebutton
exit 0
fi
@ -1698,7 +1699,8 @@ if [ $RESTART ]; then
echo "Restarting BigBlueButton $BIGBLUEBUTTON_RELEASE ..."
systemctl restart bigbluebutton.target
stop_bigbluebutton
start_bigbluebutton
check_state
fi

View File

@ -5,19 +5,19 @@
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.16.3
mongo@1.16.4
reactive-var@1.0.12
standard-minifier-css@1.8.3
standard-minifier-js@2.8.1
es5-shim@4.8.0
ecmascript@0.16.4
ecmascript@0.16.5
shell-server@0.5.0
static-html@1.3.2
react-meteor-data
session@1.2.1
tracker@1.2.1
tracker@1.3.0
check@1.3.2
rocketchat:streamer

View File

@ -1 +1 @@
METEOR@2.9.0
METEOR@2.10.0

View File

@ -1,6 +1,6 @@
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.10.1
babel-compiler@7.10.2
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
@ -8,7 +8,7 @@ blaze-tools@1.1.3
boilerplate-generator@1.7.1
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.4.0
callback-hook@1.5.0
check@1.3.2
ddp@1.4.1
ddp-client@2.6.1
@ -16,13 +16,13 @@ ddp-common@1.4.0
ddp-server@2.6.0
diff-sequence@1.1.2
dynamic-import@0.7.2
ecmascript@0.16.4
ecmascript@0.16.5
ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.3
es5-shim@4.8.0
fetch@0.1.2
fetch@0.1.3
geojson-utils@1.0.11
hot-code-push@1.0.4
html-tools@1.1.3
@ -33,7 +33,7 @@ inter-process-messaging@0.1.1
launch-screen@1.3.0
lmieulet:meteor-coverage@4.1.0
logging@1.3.1
meteor@1.10.3
meteor@1.11.0
meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
@ -46,7 +46,7 @@ mobile-status-bar@1.1.0
modern-browsers@0.1.9
modules@0.19.0
modules-runtime@0.13.1
mongo@1.16.3
mongo@1.16.4
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
@ -54,7 +54,7 @@ npm-mongo@4.12.1
ordered-dict@1.1.0
promise@0.12.2
random@1.2.1
react-fast-refresh@0.2.3
react-fast-refresh@0.2.5
react-meteor-data@2.5.1
reactive-dict@1.3.1
reactive-var@1.0.12
@ -70,9 +70,9 @@ standard-minifier-css@1.8.3
standard-minifier-js@2.8.1
static-html@1.3.2
templating-tools@1.2.2
tracker@1.2.1
typescript@4.6.4
tracker@1.3.0
typescript@4.7.4
underscore@1.0.11
url@1.3.2
webapp@1.13.2
webapp@1.13.3
webapp-hashing@1.1.1

View File

@ -94,6 +94,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
color: var(--palette-placeholder-text);
opacity: 1;
}
main {
display: initial;
}
</style>
<script>
document.addEventListener('gesturestart', function (e) {

View File

@ -48,7 +48,7 @@ export default function addGroupChatMsg(meetingId, chatId, msg) {
const insertedId = GroupChatMsg.insert(msgDocument);
if (insertedId) {
changeHasMessages(true, sender.id, meetingId);
changeHasMessages(true, sender.id, meetingId, chatId);
Logger.info(`Added group-chat-msg msgId=${msg.id} chatId=${chatId} meetingId=${meetingId}`);
}
} catch (err) {

View File

@ -1,6 +1,8 @@
import { GroupChatMsg } from '/imports/api/group-chat-msg';
import Logger from '/imports/startup/server/logger';
import addSystemMsg from '/imports/api/group-chat-msg/server/modifiers/addSystemMsg';
import clearChatHasMessages from '/imports/api/users-persistent-data/server/modifiers/clearChatHasMessages';
import UsersPersistentData from '/imports/api/users-persistent-data';
export default function clearGroupChatMsg(meetingId, chatId) {
const CHAT_CONFIG = Meteor.settings.public.chat;
@ -26,6 +28,17 @@ export default function clearGroupChatMsg(meetingId, chatId) {
message: CHAT_CLEAR_MESSAGE,
};
addSystemMsg(meetingId, PUBLIC_GROUP_CHAT_ID, clearMsg);
clearChatHasMessages(meetingId, chatId);
//clear offline users' data
const selector = {
meetingId,
'shouldPersist.hasConnectionStatus': { $ne: true },
'shouldPersist.hasMessages.private': { $ne: true },
loggedOut: true
};
UsersPersistentData.remove(selector);
}
} catch (err) {
Logger.error(`Error on clearing GroupChat (${meetingId}, ${chatId}). ${err}`);
@ -48,6 +61,8 @@ export default function clearGroupChatMsg(meetingId, chatId) {
const numberAffected = GroupChatMsg.remove({ chatId: { $eq: PUBLIC_GROUP_CHAT_ID } });
if (numberAffected) {
clearChatHasMessages(meetingId, chatId=PUBLIC_GROUP_CHAT_ID);
Logger.info('Cleared GroupChatMsg (all)');
}
} catch (err) {

View File

@ -76,6 +76,8 @@ export default function addMeeting(meeting) {
privateChatEnabled: Boolean,
captureNotes: Boolean,
captureSlides: Boolean,
captureNotesFilename: String,
captureSlidesFilename: String,
},
meetingProp: {
intId: String,

View File

@ -11,10 +11,12 @@ export default function captureSharedNotes({ header, body }) {
const {
breakoutId,
filename,
} = body;
check(breakoutId, String);
check(parentMeetingId, String);
check(filename, String);
padCapture(breakoutId, parentMeetingId);
padCapture(breakoutId, parentMeetingId, filename);
}

View File

@ -1,12 +1,12 @@
import Pads from '/imports/api/pads';
import Breakouts from '/imports/api/breakouts';
import Pads, { PadsUpdates } from '/imports/api/pads';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
export default function padCapture(breakoutId, parentMeetingId) {
export default function padCapture(breakoutId, parentMeetingId, filename) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'PadCapturePubMsg';
const EVENT_NAME_ERROR = 'PresentationConversionUpdateSysPubMsg';
const EXTERNAL_ID = Meteor.settings.public.notes.id;
try {
@ -22,21 +22,41 @@ export default function padCapture(breakoutId, parentMeetingId) {
},
);
const breakout = Breakouts.findOne({ breakoutId });
const update = PadsUpdates.findOne(
{
meetingId: breakoutId,
externalId: EXTERNAL_ID,
}, {
fields: {
rev: 1,
},
},
);
if (pad?.padId && breakout?.shortName) {
if (pad?.padId && update?.rev > 0) {
const payload = {
parentMeetingId,
breakoutId,
padId: pad.padId,
filename: `${breakout.shortName}-notes`,
filename,
};
Logger.info(`Sending PadCapturePubMsg for meetingId=${breakoutId} parentMeetingId=${parentMeetingId} padId=${pad.padId}`);
return RedisPubSub.publishMeetingMessage(CHANNEL, EVENT_NAME, parentMeetingId, payload);
}
return null;
// Notify that no content is available
const temporaryPresentationId = `${breakoutId}-notes`;
const payload = {
podId: 'DEFAULT_PRESENTATION_POD',
messageKey: '204',
code: 'not-used',
presentationId: temporaryPresentationId,
presName: filename,
temporaryPresentationId,
};
Logger.info(`No notes available for capture in meetingId=${breakoutId} parentMeetingId=${parentMeetingId} padId=${pad.padId}`);
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME_ERROR, parentMeetingId, 'system', payload);
} catch (err) {
Logger.error(`Exception while invoking method padCapture ${err.stack}`);
return null;

View File

@ -7,11 +7,10 @@ import AuthTokenValidation, {
ValidationStates,
} from '/imports/api/auth-token-validation';
import { DDPServer } from 'meteor/ddp-server';
import { publicationSafeGuard } from '/imports/api/common/server/helpers';
Meteor.server.setPublicationStrategy('polls', DDPServer.publicationStrategies.NO_MERGE);
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
function currentPoll(secretPoll) {
check(secretPoll, Boolean);
const tokenValidation = AuthTokenValidation.findOne({
@ -111,8 +110,22 @@ function polls() {
if (User) {
const poll = Polls.findOne(selector, noKeyOptions);
if (User.presenter || poll?.pollType !== 'R-') {
// Monitor this publication and stop it when user is not a presenter anymore or poll type has changed
const comparisonFunc = () => {
const user = Users.findOne({ userId, meetingId }, { fields: { role: 1, userId: 1 } });
const currentPoll = Polls.findOne(selector, noKeyOptions);
if (User.role === ROLE_MODERATOR || poll?.pollType !== 'R-') {
const condition = user.presenter || currentPoll?.pollType !== 'R-';
if (!condition) {
Logger.info(`conditions aren't filled anymore in publication ${this._name}:
user.presenter || currentPoll?.pollType !== 'R-' :${condition}, user.presenter: ${user.presenter} pollType: ${currentPoll?.pollType}`);
}
return condition;
};
publicationSafeGuard(comparisonFunc, this);
return Polls.find(selector, options);
}
}

View File

@ -14,6 +14,7 @@ const GENERATED_SLIDE_KEY = 'GENERATED_SLIDE';
const FILE_TOO_LARGE_KEY = 'FILE_TOO_LARGE';
const CONVERSION_TIMEOUT_KEY = "CONVERSION_TIMEOUT";
const IVALID_MIME_TYPE_KEY = "IVALID_MIME_TYPE";
const NO_CONTENT = '204';
// const GENERATING_THUMBNAIL_KEY = 'GENERATING_THUMBNAIL';
// const GENERATED_THUMBNAIL_KEY = 'GENERATED_THUMBNAIL';
// const GENERATING_TEXTFILES_KEY = 'GENERATING_TEXTFILES';
@ -76,6 +77,13 @@ export default function handlePresentationConversionUpdate({ body }, meetingId)
statusModifier['conversion.numPages'] = body.numberOfPages;
break;
case NO_CONTENT:
statusModifier['conversion.done'] = false;
statusModifier['conversion.error'] = true;
statusModifier.id = presentationId;
statusModifier.name = presentationName;
break;
default:
break;
}

View File

@ -1,15 +1,20 @@
import Logger from '/imports/startup/server/logger';
import UsersPersistentData from '/imports/api/users-persistent-data';
export default function changeHasMessages(hasMessages, userId, meetingId) {
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
export default function changeHasMessages(hasMessages, userId, meetingId, chatId) {
const selector = {
meetingId,
userId,
};
const type = chatId === PUBLIC_GROUP_CHAT_KEY ? 'public' : 'private';
const modifier = {
$set: {
'shouldPersist.hasMessages': hasMessages,
[`shouldPersist.hasMessages.${type}`]: hasMessages,
},
};

View File

@ -0,0 +1,29 @@
import Logger from '/imports/startup/server/logger';
import UsersPersistentData from '/imports/api/users-persistent-data';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
export default function clearChatHasMessages(meetingId, chatId) {
const selector = {
meetingId,
};
const type = chatId === PUBLIC_GROUP_CHAT_KEY ? 'public' : 'private';
const modifier = {
$set: {
[`shouldPersist.hasMessages.${type}`]: false,
},
};
try {
const numberAffected = UsersPersistentData.update(selector, modifier, { multi: true });
if (numberAffected) {
Logger.info(`Cleared hasMessages meeting=${meetingId}`);
}
} catch (err) {
Logger.error(`Clear hasMessages error: ${err}`);
}
}

View File

@ -3,6 +3,7 @@ import Users from '/imports/api/users';
import { extractCredentials } from '/imports/api/common/server/helpers';
import ClientConnections from '/imports/startup/server/ClientConnections';
import { check } from 'meteor/check';
import UsersPersistentData from '/imports/api/users-persistent-data';
export default function userLeftMeeting() { // TODO-- spread the code to method/modifier/handler
try {
@ -20,6 +21,7 @@ export default function userLeftMeeting() { // TODO-- spread the code to method/
const numberAffected = Users.update(selector, { $set: { loggedOut: true } });
if (numberAffected) {
UsersPersistentData.update(selector, { $set: { loggedOut: true } });
Logger.info(`user left id=${requesterUserId} meeting=${meetingId}`);
ClientConnections.removeClientConnection(this.userId, this.connection.id);
}

View File

@ -48,7 +48,7 @@ export default function removeUser(body, meetingId) {
clearUserInfoForRequester(meetingId, userId);
const currentUser = UsersPersistentData.findOne({ userId, meetingId });
const hasMessages = currentUser?.shouldPersist?.hasMessages;
const hasMessages = currentUser?.shouldPersist?.hasMessages?.public || currentUser?.shouldPersist?.hasMessages?.private;
const hasConnectionStatus = currentUser?.shouldPersist?.hasConnectionStatus;
if (!hasMessages && !hasConnectionStatus) {

View File

@ -261,7 +261,15 @@ class Base extends Component {
if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) {
return (<LoadingScreen>{loading}</LoadingScreen>);
}
if (( meetingHasEnded || ejected || userRemoved ) && meetingIsBreakout) {
Base.setExitReason('breakoutEnded').finally(() => {
Meteor.disconnect();
window.close();
});
return null;
}
if (ejected) {
return (
<MeetingEnded
@ -272,14 +280,6 @@ class Base extends Component {
);
}
if (meetingHasEnded && meetingIsBreakout) {
Base.setExitReason('breakoutEnded').finally(() => {
Meteor.disconnect();
window.close();
});
return null;
}
if (meetingHasEnded && !meetingIsBreakout) {
return (
<MeetingEnded

View File

@ -12,6 +12,7 @@ import Styled from './styles';
import Icon from '/imports/ui/components/common/icon/component.jsx';
import { isImportSharedNotesFromBreakoutRoomsEnabled, isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled } from '/imports/ui/services/features';
import { addNewAlert } from '/imports/ui/components/screenreader-alert/service';
import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
@ -92,6 +93,14 @@ const intlMessages = defineMessages({
id: 'app.createBreakoutRoom.captureSlides',
description: 'capture slides label',
},
captureNotesType: {
id: 'app.notes.label',
description: 'indicates notes have been captured',
},
captureSlidesType: {
id: 'app.shortcut-help.whiteboard',
description: 'indicates the whiteboard has been captured',
},
roomLabel: {
id: 'app.createBreakoutRoom.room',
description: 'Room label',
@ -183,7 +192,7 @@ const propTypes = {
getBreakouts: PropTypes.func.isRequired,
sendInvitation: PropTypes.func.isRequired,
mountModal: PropTypes.func.isRequired,
isBreakoutRecordable: PropTypes.bool.isRequired,
isBreakoutRecordable: PropTypes.bool,
};
class BreakoutRoom extends PureComponent {
@ -251,6 +260,8 @@ class BreakoutRoom extends PureComponent {
componentDidMount() {
const {
breakoutJoinedUsers, getLastBreakouts, groups, isUpdate,
setAllowUserToChooseABreakout, setCaptureBreakoutNotes,
setCaptureBreakoutWhiteboard,
} = this.props;
this.setRoomUsers();
if (isUpdate) {
@ -276,6 +287,11 @@ class BreakoutRoom extends PureComponent {
};
});
}
this.setState({
freeJoin: setAllowUserToChooseABreakout,
captureSlides: setCaptureBreakoutWhiteboard,
captureNotes: setCaptureBreakoutNotes,
});
const lastBreakouts = getLastBreakouts();
if (lastBreakouts.length > 0) {
@ -445,6 +461,8 @@ class BreakoutRoom extends PureComponent {
const rooms = _.range(1, numberOfRooms + 1).map((seq) => ({
users: this.getUserByRoom(seq).map((u) => u.userId),
name: this.getFullName(seq),
captureNotesFilename: this.getCaptureFilename(seq, false),
captureSlidesFilename: this.getCaptureFilename(seq),
shortName: this.getRoomName(seq),
isDefaultName: !this.hasNameChanged(seq),
freeJoin,
@ -607,7 +625,7 @@ class BreakoutRoom extends PureComponent {
return breakoutJoinedUsers.filter((room) => room.sequence === sequence)[0].joinedUsers || [];
}
getRoomName(position) {
getRoomName(position, padWithZeroes = false) {
const { intl } = this.props;
const { roomNamesChanged } = this.state;
@ -615,7 +633,9 @@ class BreakoutRoom extends PureComponent {
return roomNamesChanged[position];
}
return intl.formatMessage(intlMessages.breakoutRoom, { 0: position });
return intl.formatMessage(intlMessages.breakoutRoom, {
0: padWithZeroes ? `${position}`.padStart(2, '0') : position
});
}
getFullName(position) {
@ -624,6 +644,22 @@ class BreakoutRoom extends PureComponent {
return `${meetingName} (${this.getRoomName(position)})`;
}
getCaptureFilename(position, slides = true) {
const { intl } = this.props;
const presentations = PresentationUploaderService.getPresentations();
const captureType = slides
? intl.formatMessage(intlMessages.captureSlidesType)
: intl.formatMessage(intlMessages.captureNotesType);
const fileName = `${this.getRoomName(position,true)}_${captureType}`.replace(/ /g, '_');
const fileNameDuplicatedCount = presentations.filter((pres) => pres.filename?.startsWith(fileName)
|| pres.name?.startsWith(fileName)).length;
return fileNameDuplicatedCount === 0 ? fileName : `${fileName}(${fileNameDuplicatedCount + 1})`;
}
resetUserWhenRoomsChange(rooms) {
const { users } = this.state;
const filtredUsers = users.filter((u) => u.room > rooms);

View File

@ -2,15 +2,29 @@ import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import ActionsBarService from '/imports/ui/components/actions-bar/service';
import BreakoutRoomService from '/imports/ui/components/breakout-room/service';
import CreateBreakoutRoomModal from './component';
import { isImportSharedNotesFromBreakoutRoomsEnabled, isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled } from '/imports/ui/services/features';
const METEOR_SETTINGS_APP = Meteor.settings.public.app;
const CreateBreakoutRoomContainer = (props) => {
const { setAllowUserToChooseABreakout } = METEOR_SETTINGS_APP.breakouts;
const setCaptureBreakoutWhiteboard = METEOR_SETTINGS_APP.breakouts.setCaptureBreakoutWhiteboard
&& isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled();
const setCaptureBreakoutNotes = METEOR_SETTINGS_APP.breakouts.setCaptureBreakoutNotes
&& isImportSharedNotesFromBreakoutRoomsEnabled();
const { amIModerator } = props;
return (
amIModerator
&& (
<CreateBreakoutRoomModal {...props} />
<CreateBreakoutRoomModal
{...props}
{...{
setAllowUserToChooseABreakout,
setCaptureBreakoutWhiteboard,
setCaptureBreakoutNotes,
}}
/>
)
);
};

View File

@ -559,7 +559,13 @@ class App extends Component {
? <ExternalVideoContainer isLayoutSwapped={!presentationIsOpen} isPresenter={isPresenter} />
: null
}
{shouldShowSharedNotes ? <NotesContainer area="media" layoutType={selectedLayout} /> : null}
{shouldShowSharedNotes
? (
<NotesContainer
area="media"
layoutType={selectedLayout}
/>
) : null}
{this.renderCaptions()}
<AudioCaptionsSpeechContainer />
{this.renderAudioCaptions()}

View File

@ -64,6 +64,9 @@ const Select = ({
locale,
voices,
}) => {
const useLocaleHook = SpeechService.useFixedLocale();
if (!enabled || useLocaleHook) return null;
if (voices.length === 0) {
return (
<div
@ -77,8 +80,6 @@ const Select = ({
);
}
if (!enabled || SpeechService.useFixedLocale()) return null;
const onChange = (e) => {
const { value } = e.target;
SpeechService.setSpeechLocale(value);

View File

@ -7,11 +7,11 @@ import logger from '/imports/startup/client/logger';
import Users from '/imports/api/users';
import AudioService from '/imports/ui/components/audio/service';
import deviceInfo from '/imports/utils/deviceInfo';
import { isLiveTranscriptionEnabled } from '/imports/ui/services/features';
const THROTTLE_TIMEOUT = 1000;
const CONFIG = Meteor.settings.public.app.audioCaptions;
const ENABLED = CONFIG.enabled;
const LANGUAGES = CONFIG.language.available;
const VALID_ENVIRONMENT = !deviceInfo.isMobile || CONFIG.mobile;
@ -126,7 +126,7 @@ const hasSpeechLocale = (userId = Auth.userID) => getSpeechLocale(userId) !== ''
const isLocaleValid = (locale) => LANGUAGES.includes(locale);
const isEnabled = () => ENABLED;
const isEnabled = () => isLiveTranscriptionEnabled();
const isActive = () => isEnabled() && hasSpeechRecognitionSupport() && hasSpeechLocale();

View File

@ -430,7 +430,7 @@ class BreakoutRoom extends PureComponent {
const { animations } = Settings.application;
const roomItems = breakoutRooms.map((breakout) => (
<Styled.BreakoutItems key={`breakoutRoomItems-${breakout.breakoutId}`} >
<Styled.BreakoutItems key={`breakoutRoomItems-${breakout.breakoutId}`}>
<Styled.Content key={`breakoutRoomList-${breakout.breakoutId}`}>
<Styled.BreakoutRoomListNameLabel data-test={breakout.shortName} aria-hidden>
{breakout.isDefaultName

View File

@ -10,7 +10,7 @@ const BREAKOUT_MODAL_DELAY = 200;
const propTypes = {
mountModal: PropTypes.func.isRequired,
lastBreakoutReceived: PropTypes.shape({
breakoutUrlData: PropTypes.func.isRequired,
breakoutUrlData: PropTypes.object.isRequired,
}),
breakoutRoomsUserIsIn: PropTypes.shape({
sequence: PropTypes.number.isRequired,

View File

@ -7,7 +7,6 @@ import UserListService from '/imports/ui/components/user-list/service';
import fp from 'lodash/fp';
import UsersPersistentData from '/imports/api/users-persistent-data';
import { UploadingPresentations } from '/imports/api/presentations';
import _ from 'lodash';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
@ -43,7 +42,7 @@ const upsertCapturedContent = (filename, temporaryPresentationId) => {
temporaryPresentationId,
}, {
$set: {
id: _.uniqueId(filename),
id: temporaryPresentationId,
temporaryPresentationId,
progress: 0,
filename,
@ -62,6 +61,7 @@ const setCapturedContentUploading = () => {
breakoutRooms.forEach((breakout) => {
const filename = breakout.shortName;
const temporaryPresentationId = breakout.breakoutId;
if (breakout.captureNotes) {
upsertCapturedContent(filename, `${temporaryPresentationId}-notes`);
}

View File

@ -33,6 +33,7 @@ const CaptionsButton = ({ intl, isActive, handleOnClick }) => (
size="lg"
onClick={handleOnClick}
id={isActive ? 'stop-captions-button' : 'start-captions-button'}
data-test="startViewingClosedCaptionsBtn"
/>
);

View File

@ -71,7 +71,7 @@ class LiveCaptions extends PureComponent {
return (
<div>
<div style={captionStyles}>
<div data-test="liveCaptions" style={captionStyles}>
{clear ? '' : data}
</div>
<div style={visuallyHidden}>

View File

@ -395,6 +395,7 @@ class ReaderMenu extends PureComponent {
label={intl.formatMessage(intlMessages.start)}
onClick={() => this.handleStart()}
disabled={locale == null}
data-test="startViewingClosedCaptions"
/>
</Styled.Actions>
</Styled.Footer>

View File

@ -140,6 +140,7 @@ class WriterMenu extends PureComponent {
aria-describedby="descriptionStart"
onClick={this.handleStart}
disabled={locale == null}
data-test="startWritingClosedCaptions"
/>
<div id="descriptionStart" hidden>{intl.formatMessage(intlMessages.ariaStartDesc)}</div>
</Styled.Content>

View File

@ -338,7 +338,7 @@ class ConnectionStatusComponent extends PureComponent {
if (isConnectionStatusEmpty(connectionStatus)) return this.renderEmpty();
let connections = connectionStatus;
if (selectedTab === 2) {
if (selectedTab === 1) {
connections = connections.filter(conn => conn.you);
if (isConnectionStatusEmpty(connections)) return this.renderEmpty();
}

View File

@ -100,7 +100,7 @@ const intlMessage = defineMessages({
},
user_inactivity_eject_reason: {
id: 'app.meeting.logout.userInactivityEjectReason',
description: 'message for whom was kicked by inactivity',
description: 'message to whom was kicked by inactivity',
},
open_activity_report_btn: {
id: 'app.learning-dashboard.clickHereToOpen',
@ -142,7 +142,7 @@ class MeetingEnded extends PureComponent {
this.localUserRole = user.role;
}
const meeting = Meetings.findOne({ id: user.meetingID });
const meeting = Meetings.findOne({ id: user?.meetingID });
if (meeting) {
this.endWhenNoModeratorMinutes = meeting.durationProps.endWhenNoModeratorDelayInMinutes;

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
@ -12,7 +12,7 @@ import NotesDropdown from '/imports/ui/components/notes/notes-dropdown/container
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
const DELAY_UNMOUNT_SHARED_NOTES = Meteor.settings.public.app.delayForUnmountOfSharedNote;
const intlMessages = defineMessages({
hide: {
id: 'app.notes.hide',
@ -47,6 +47,8 @@ const defaultProps = {
layoutType: null,
};
let timoutRef = null;
const sidebarContentToIgnoreDelay = ['captions'];
const Notes = ({
hasPermission,
intl,
@ -58,18 +60,38 @@ const Notes = ({
sidebarContent,
sharedNotesOutput,
amIPresenter,
isToSharedNotesBeShow,
shouldShowSharedNotesOnPresentationArea,
}) => {
useEffect(() => () => Service.setLastRev(), []);
const [shouldRenderNotes, setShouldRenderNotes] = useState(false);
const { isChrome } = browserInfo;
const isOnMediaArea = area === 'media';
const style = isOnMediaArea ? {
position: 'absolute',
...sharedNotesOutput,
} : {};
const isHidden = isOnMediaArea && (style.width === 0 || style.height === 0);
if (isHidden) style.padding = 0;
const isHidden = (isOnMediaArea && (style.width === 0 || style.height === 0))
|| (!isToSharedNotesBeShow
&& !sidebarContentToIgnoreDelay.includes(sidebarContent.sidebarContentPanel))
|| shouldShowSharedNotesOnPresentationArea;
if (isHidden) {
style.padding = 0;
}
useEffect(() => {
if (isToSharedNotesBeShow) {
setShouldRenderNotes(true);
clearTimeout(timoutRef);
} else {
timoutRef = setTimeout(() => {
setShouldRenderNotes(false);
}, (sidebarContentToIgnoreDelay.includes(sidebarContent.sidebarContentPanel)
|| shouldShowSharedNotesOnPresentationArea)
? 0 : DELAY_UNMOUNT_SHARED_NOTES);
}
}, [isToSharedNotesBeShow, sidebarContent.sidebarContentPanel]);
useEffect(() => {
if (
isOnMediaArea
@ -128,7 +150,7 @@ const Notes = ({
) : null;
};
return (
return (shouldRenderNotes || shouldShowSharedNotesOnPresentationArea) && (
<Styled.Notes data-test="notes" isChrome={isChrome} style={style}>
{!isOnMediaArea ? (
<Header

View File

@ -3,6 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data';
import Notes from './component';
import Service from './service';
import Auth from '/imports/ui/services/auth';
import MediaService from '/imports/ui/components/media/service';
import { layoutSelectInput, layoutDispatch, layoutSelectOutput } from '../layout/context';
import { UsersContext } from '../components-data/users-context/context';
@ -29,9 +30,10 @@ const Container = ({ ...props }) => {
export default withTracker(() => {
const hasPermission = Service.hasPermission();
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
const shouldShowSharedNotesOnPresentationArea = MediaService.shouldShowSharedNotes();
return {
hasPermission,
isRTL,
shouldShowSharedNotesOnPresentationArea,
};
})(Container);

View File

@ -293,7 +293,7 @@ const PresentationMenu = (props) => {
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
container: fullscreenRef
}}
actions={getAvailableOptions()}
actions={options}
/>
</Styled.Right>
);

View File

@ -81,6 +81,10 @@ const intlMessages = defineMessages({
id: 'app.presentationUploder.conversion.unsupportedDocument',
description: 'warns the user that the file extension is not supported',
},
204: {
id: 'app.presentationUploder.conversion.204',
description: 'error indicating that the file has no content to capture',
},
fileToUpload: {
id: 'app.presentationUploder.fileToUpload',
description: 'message used in the file selected for upload',

View File

@ -134,10 +134,12 @@ const SidebarContent = (props) => {
<ErrorBoundary
Fallback={FallbackView}
>
<ChatContainer width={width}/>
<ChatContainer width={width} />
</ErrorBoundary>
)}
{sidebarContentPanel === PANELS.SHARED_NOTES && <NotesContainer />}
<NotesContainer
isToSharedNotesBeShow={sidebarContentPanel === PANELS.SHARED_NOTES}
/>
{sidebarContentPanel === PANELS.CAPTIONS && <CaptionsContainer />}
{sidebarContentPanel === PANELS.BREAKOUT && <BreakoutRoomContainer />}
{sidebarContentPanel === PANELS.WAITING_USERS && <WaitingUsersPanel />}

View File

@ -13,8 +13,6 @@ const SidebarContentContainer = () => {
const { users } = usingUsersContext;
const amIPresenter = users[Auth.meetingID][Auth.userID].presenter;
if (sidebarContentOutput.display === false) return null;
return (
<SidebarContent
{...sidebarContentOutput}

View File

@ -308,6 +308,7 @@ class UserOptions extends PureComponent {
description: intl.formatMessage(intlMessages.captionsDesc),
key: this.captionsId,
onClick: this.handleCaptionsClick,
dataTest: 'writeClosedCaptions',
});
}
if (amIModerator) {

View File

@ -1,5 +1,5 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import PropTypes from 'prop-types';
import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/api/meetings';
import ActionsBarService from '/imports/ui/components/actions-bar/service';
@ -12,12 +12,6 @@ import { notify } from '/imports/ui/services/notification';
import UserOptions from './component';
import { layoutSelect } from '/imports/ui/components/layout/context';
const propTypes = {
users: PropTypes.arrayOf(Object).isRequired,
clearAllEmojiStatus: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
const intlMessages = defineMessages({
clearStatusMessage: {
id: 'app.userList.content.participants.options.clearedStatus',
@ -32,14 +26,26 @@ const meetingMuteDisabledLog = () => logger.info({
extraInfo: { logType: 'moderator_action' },
}, 'moderator disabled meeting mute');
const UserOptionsContainer = withTracker((props) => {
const UserOptionsContainer = (props) => {
const isRTL = layoutSelect((i) => i.isRTL);
return (
<UserOptions
{...props}
{...{
isRTL
}}
/>
)
};
export default injectIntl(withTracker((props) => {
const {
users,
clearAllEmojiStatus,
intl,
isMeetingMuteOnStart,
} = props;
const toggleStatus = () => {
clearAllEmojiStatus(users);
@ -54,9 +60,6 @@ const UserOptionsContainer = withTracker((props) => {
const { name } = meetingProp;
return name;
};
const isRTL = layoutSelect((i) => i.isRTL);
return {
toggleMuteAllUsers: () => {
UserListService.muteAllUsers(Auth.userID);
@ -88,10 +91,5 @@ const UserOptionsContainer = withTracker((props) => {
meetingName: getMeetingName(),
openLearningDashboardUrl: LearningDashboardService.openLearningDashboardUrl,
dynamicGuestPolicy,
isRTL,
};
})(UserOptions);
UserOptionsContainer.propTypes = propTypes;
export default injectIntl(UserOptionsContainer);
})(UserOptionsContainer));

View File

@ -1,6 +1,6 @@
import * as React from "react";
import _ from "lodash";
import { createGlobalStyle } from "styled-components";
import styled, { createGlobalStyle } from "styled-components";
import Cursors from "./cursors/container";
import { TldrawApp, Tldraw } from "@tldraw/tldraw";
import SlideCalcUtil, {HUNDRED_PERCENT} from '/imports/utils/slideCalcUtils';
@ -23,25 +23,20 @@ const findRemoved = (A, B) => {
// map different localeCodes from bbb to tldraw
const mapLanguage = (language) => {
switch(language) {
case 'fa-ir':
return 'fa';
case 'it-it':
return 'it';
// bbb has xx-xx but in tldraw it's only xx
if (['es', 'fa', 'it', 'pl', 'sv', 'uk'].some((lang) => language.startsWith(lang))) {
return language.substring(0, 2);
}
// exceptions
switch (language) {
case 'nb-no':
return 'no';
case 'pl-pl':
return 'pl';
case 'sv-se':
return 'sv';
case 'uk-ua':
return 'uk';
case 'zh-cn':
return 'zh-ch';
default:
return language;
}
}
};
const SMALL_HEIGHT = 435;
const SMALLEST_HEIGHT = 363;
@ -57,14 +52,37 @@ const TldrawGlobalStyle = createGlobalStyle`
display: none;
}
`}
${({ hideCursor }) => hideCursor && `
#canvas {
cursor: none;
}
`}
#TD-PrimaryTools-Image {
display: none;
}
#slide-background-shape div {
pointer-events: none;
}
[aria-expanded*="false"][aria-controls*="radix-"] {
display: none;
}
${({ hasWBAccess, isPresenter, size }) => (hasWBAccess || isPresenter) && `
#TD-Tools-Dots {
height: ${size}px;
width: ${size}px;
}
#TD-Delete {
& button {
height: ${size}px;
width: ${size}px;
}
}
#TD-PrimaryTools button {
height: ${size}px;
width: ${size}px;
}
`}
`;
const EditableWBWrapper = styled.div`
&, & > :first-child {
cursor: inherit !important;
}
`;
export default function Whiteboard(props) {
@ -97,6 +115,7 @@ export default function Whiteboard(props) {
intl,
svgUri,
maxStickyNoteLength,
fontFamily,
} = props;
const { pages, pageStates } = initDefaultPages(curPres?.pages.length || 1);
@ -119,6 +138,7 @@ export default function Whiteboard(props) {
const prevFitToWidth = usePrevious(fitToWidth);
const prevSvgUri = usePrevious(svgUri);
const language = mapLanguage(Settings?.application?.locale?.toLowerCase() || 'en');
const [currentTool, setCurrentTool] = React.useState(null);
const calculateZoom = (width, height) => {
let zoom = fitToWidth
@ -138,10 +158,19 @@ export default function Whiteboard(props) {
return hasShapeAccess;
}
const isValidShapeType = (shape) => {
const invalidTypes = ['image', 'video'];
return !invalidTypes.includes(shape?.type);
}
const sendShapeChanges= (app, changedShapes, redo = false) => {
const invalidChange = Object.keys(changedShapes)
.find(id => !hasShapeAccess(id));
if (invalidChange) {
.find(id => !hasShapeAccess(id));
const invalidShapeType = Object.keys(changedShapes)
.find(id => !isValidShapeType(changedShapes[id]));
if (invalidChange || invalidShapeType) {
notifyNotAllowedChange(intl);
// undo last command without persisting to not generate the onUndo/onRedo callback
if (!redo) {
@ -197,6 +226,35 @@ export default function Whiteboard(props) {
props.setTldrawIsMounting(true);
}, []);
const checkClientBounds = (e) => {
if (
e.clientX > document.documentElement.clientWidth ||
e.clientX < 0 ||
e.clientY > document.documentElement.clientHeight ||
e.clientY < 0
) {
if (tldrawAPI?.session) {
tldrawAPI?.completeSession?.();
}
}
};
const checkVisibility = () => {
if (document.visibilityState === 'hidden' && tldrawAPI?.session) {
tldrawAPI?.completeSession?.();
}
};
React.useEffect(() => {
document.addEventListener('mouseup', checkClientBounds);
document.addEventListener('visibilitychange', checkVisibility);
return () => {
document.removeEventListener('mouseup', checkClientBounds);
document.removeEventListener('visibilitychange', checkVisibility);
};
}, [tldrawAPI]);
const doc = React.useMemo(() => {
const currentDoc = rDocument.current;
@ -249,6 +307,7 @@ export default function Whiteboard(props) {
},
},
);
changed = true;
}
if (changed && tldrawAPI) {
@ -268,7 +327,7 @@ export default function Whiteboard(props) {
}
// move poll result text to bottom right
if (next.pages[curPageId]) {
if (next.pages[curPageId] && slidePosition) {
const pollResults = Object.entries(next.pages[curPageId].shapes)
.filter(([id, shape]) => shape.name?.includes("poll-result"))
for (const [id, shape] of pollResults) {
@ -317,7 +376,7 @@ export default function Whiteboard(props) {
}, [presentationWidth, presentationHeight, curPageId, document?.documentElement?.dir]);
React.useEffect(() => {
if (presentationWidth > 0 && presentationHeight > 0) {
if (presentationWidth > 0 && presentationHeight > 0 && slidePosition) {
const cameraZoom = tldrawAPI?.getPageState()?.camera?.zoom;
const newzoom = calculateZoom(slidePosition.viewBoxWidth, slidePosition.viewBoxHeight);
if (cameraZoom && cameraZoom === 1) {
@ -342,7 +401,7 @@ export default function Whiteboard(props) {
// change tldraw page when presentation page changes
React.useEffect(() => {
if (tldrawAPI && curPageId) {
if (tldrawAPI && curPageId && slidePosition) {
tldrawAPI.changePage(curPageId);
let zoom = prevSlidePosition
? calculateZoom(prevSlidePosition.viewBoxWidth, prevSlidePosition.viewBoxHeight)
@ -407,40 +466,13 @@ export default function Whiteboard(props) {
React.useEffect(() => {
if (hasWBAccess || isPresenter) {
tldrawAPI?.setSetting('dockPosition', isRTL ? 'left' : 'right');
const tdToolsDots = document.getElementById("TD-Tools-Dots");
const tdDelete = document.getElementById("TD-Delete");
const tdPrimaryTools = document.getElementById("TD-PrimaryTools");
const tdTools = document.getElementById("TD-Tools");
if (tdToolsDots && tdDelete && tdPrimaryTools) {
const size = ((props.height < SMALL_HEIGHT) || (props.width < SMALL_WIDTH))
? TOOLBAR_SMALL : TOOLBAR_LARGE;
tdToolsDots.style.height = `${size}px`;
tdToolsDots.style.width = `${size}px`;
const delButton = tdDelete.getElementsByTagName('button')[0];
delButton.style.height = `${size}px`;
delButton.style.width = `${size}px`;
const primaryBtns = tdPrimaryTools?.getElementsByTagName('button');
for (let item of primaryBtns) {
item.style.height = `${size}px`;
item.style.width = `${size}px`;
}
}
if (((props.height < SMALLEST_HEIGHT) || (props.width < SMALLEST_WIDTH)) && tdTools) {
if (((props.height < SMALLEST_HEIGHT) || (props.width < SMALLEST_WIDTH))) {
tldrawAPI?.setSetting('dockPosition', 'bottom');
tdTools.parentElement.style.bottom = `${TOOLBAR_OFFSET}px`;
} else {
tldrawAPI?.setSetting('dockPosition', isRTL ? 'left' : 'right');
}
// removes tldraw native help menu button
tdTools?.parentElement?.nextSibling?.remove();
// removes image tool from the tldraw toolbar
document.getElementById("TD-PrimaryTools-Image").style.display = 'none';
}
if (tldrawAPI) {
tldrawAPI.isForcePanning = isPanning;
}
});
}, [props.height, props.width]);
React.useEffect(() => {
if (tldrawAPI) {
@ -482,6 +514,7 @@ export default function Whiteboard(props) {
appState: {
currentStyle: {
textAlign: isRTL ? "end" : "start",
font: fontFamily,
},
},
}
@ -546,7 +579,7 @@ export default function Whiteboard(props) {
}
}
if (reason && isPresenter && (reason.includes("zoomed") || reason.includes("panned"))) {
if (reason && isPresenter && slidePosition && (reason.includes("zoomed") || reason.includes("panned"))) {
const camera = tldrawAPI.getPageState()?.camera;
// limit bounds
@ -608,36 +641,30 @@ export default function Whiteboard(props) {
}
}
if (reason && reason === 'patched_shapes' && e?.session?.type === "edit" && e?.session?.initialShape?.type === "text") {
if (reason && reason === 'patched_shapes' && e?.session?.type === 'edit') {
const patchedShape = e?.getShape(e?.getPageState()?.editingId);
if (!shapes[patchedShape.id]) {
if (e?.session?.initialShape?.type === 'sticky' && patchedShape?.text?.length > maxStickyNoteLength) {
patchedShape.text = patchedShape.text.substring(0, maxStickyNoteLength);
}
if (e?.session?.initialShape?.type === 'text' && !shapes[patchedShape.id]) {
patchedShape.userId = currentUser?.userId;
persistShape(patchedShape, whiteboardId);
} else {
const diff = {
id: patchedShape.id,
point: patchedShape.point,
text: patchedShape.text
}
text: patchedShape.text,
};
persistShape(diff, whiteboardId);
}
}
if (reason && reason === 'patched_shapes') {
const patchedShape = e?.getShape(e?.getPageState()?.editingId);
if (reason && reason.includes('selected_tool')) {
const tool = reason.split(':')[1];
if (e?.session?.initialShape?.type === "sticky" && patchedShape?.text?.length > maxStickyNoteLength) {
patchedShape.text = patchedShape.text.substring(0, maxStickyNoteLength);
}
if (patchedShape) {
const diff = {
id: patchedShape.id,
point: patchedShape.point,
text: patchedShape.text
}
persistShape(diff, whiteboardId);
}
setCurrentTool(tool);
}
};
@ -704,22 +731,10 @@ export default function Whiteboard(props) {
}
};
const onPaste = (e) => {
// disable file pasting
const clipboardData = e.clipboardData || window.clipboardData;
const { types } = clipboardData;
const hasFiles = types && types.indexOf && types.indexOf('Files') !== -1;
if (hasFiles) {
e.preventDefault();
e.stopPropagation();
}
};
const webcams = document.getElementById('cameraDock');
const dockPos = webcams?.getAttribute("data-position");
const editableWB = (
<div onPaste={onPaste}>
<EditableWBWrapper>
<Tldraw
key={`wb-${isRTL}-${dockPos}-${forcePanning}`}
document={doc}
@ -741,7 +756,7 @@ export default function Whiteboard(props) {
onRedo={onRedo}
onCommand={onCommand}
/>
</div>
</EditableWBWrapper>
);
const readOnlyWB = (
@ -765,6 +780,13 @@ export default function Whiteboard(props) {
/>
);
const size = ((props.height < SMALL_HEIGHT) || (props.width < SMALL_WIDTH))
? TOOLBAR_SMALL : TOOLBAR_LARGE;
if (isPanning && tldrawAPI) {
tldrawAPI.isForcePanning = isPanning;
}
return (
<>
<Cursors
@ -775,11 +797,14 @@ export default function Whiteboard(props) {
isViewersCursorLocked={isViewersCursorLocked}
isMultiUserActive={isMultiUserActive}
isPanning={isPanning}
currentTool={currentTool}
>
{hasWBAccess || isPresenter ? editableWB : readOnlyWB}
<TldrawGlobalStyle
hideContextMenu={!hasWBAccess && !isPresenter}
hideCursor={!isPanning && (isPresenter || hasWBAccess)}
<TldrawGlobalStyle
hasWBAccess={hasWBAccess}
isPresenter={isPresenter}
hideContextMenu={!hasWBAccess && !isPresenter}
size={size}
/>
</Cursors>
</>

View File

@ -26,7 +26,8 @@ const WhiteboardContainer = (props) => {
const isPresenter = currentUser.presenter;
const isModerator = currentUser.role === ROLE_MODERATOR;
const maxStickyNoteLength = WHITEBOARD_CONFIG.maxStickyNoteLength;
return <Whiteboard {...{ isPresenter, isModerator, currentUser, isRTL, width, height, maxStickyNoteLength }} {...props} meetingId={Auth.meetingID} />
const fontFamily = WHITEBOARD_CONFIG.styles.text.family;
return <Whiteboard {...{ isPresenter, isModerator, currentUser, isRTL, width, height, maxStickyNoteLength, fontFamily }} {...props} meetingId={Auth.meetingID} />
};
export default withTracker(({ whiteboardId, curPageId, intl, zoomChanger, slidePosition, svgUri }) => {

View File

@ -1,4 +1,5 @@
import * as React from 'react';
import { Meteor } from 'meteor/meteor';
const XS_OFFSET = 8;
const SMALL_OFFSET = 18;
@ -6,6 +7,23 @@ const XL_OFFSET = 85;
const BOTTOM_CAM_HANDLE_HEIGHT = 10;
const PRES_TOOLBAR_HEIGHT = 35;
const baseName = Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename;
const makeCursorUrl = (filename) => `${baseName}/resources/images/whiteboard-cursor/${filename}`;
const TOOL_CURSORS = {
select: 'none',
erase: 'none',
arrow: 'none',
draw: `url('${makeCursorUrl('pencil.png')}') 2 22, default`,
rectangle: `url('${makeCursorUrl('square.png')}'), default`,
ellipse: `url('${makeCursorUrl('ellipse.png')}'), default`,
triangle: `url('${makeCursorUrl('triangle.png')}'), default`,
line: `url('${makeCursorUrl('line.png')}'), default`,
text: `url('${makeCursorUrl('text.png')}'), default`,
sticky: `url('${makeCursorUrl('square.png')}'), default`,
pan: `url('${makeCursorUrl('pan.png')}'), default`,
};
const Cursor = (props) => {
const {
name,
@ -130,6 +148,7 @@ export default function Cursors(props) {
hasMultiUserAccess,
isMultiUserActive,
isPanning,
currentTool,
} = props;
const start = () => setActive(true);
@ -311,8 +330,8 @@ export default function Cursors(props) {
});
const multiUserAccess = hasMultiUserAccess(whiteboardId, currentUser?.userId);
let cursorType = multiUserAccess || currentUser?.presenter ? 'none' : 'default';
if (isPanning) cursorType = 'grab';
let cursorType = multiUserAccess || currentUser?.presenter ? TOOL_CURSORS[currentTool] || 'none' : 'default';
if (isPanning) cursorType = TOOL_CURSORS.pan;
return (
<span ref={(r) => { cursorWrapper = r; }}>

View File

@ -8,6 +8,7 @@ import allowRedirectToLogoutURL from '/imports/ui/components/meeting-ended/servi
import { initCursorStreamListener } from '/imports/ui/components/cursor/service';
import SubscriptionRegistry from '/imports/ui/services/subscription-registry/subscriptionRegistry';
import { ValidationStates } from '/imports/api/auth-token-validation';
import logger from '/imports/startup/client/logger';
const CONNECTION_TIMEOUT = Meteor.settings.public.app.connectionTimeout;
@ -217,6 +218,9 @@ class Auth {
this.loggedIn = true;
this.uniqueClientSession = `${this.sessionToken}-${Math.random().toString(36).substring(6)}`;
})
.catch((err) => {
logger.error(`Failed to validate token: ${err.description}`);
})
.finally(() => {
this.isAuthenticating = false;
});

View File

@ -40,6 +40,10 @@ export function isCaptionsEnabled() {
return getDisabledFeatures().indexOf('captions') === -1 && Meteor.settings.public.captions.enabled;
}
export function isLiveTranscriptionEnabled() {
return getDisabledFeatures().indexOf('liveTranscription') === -1 && Meteor.settings.public.app.audioCaptions.enabled;
}
export function isBreakoutRoomsEnabled() {
return getDisabledFeatures().indexOf('breakoutRooms') === -1;
}

View File

@ -11,7 +11,7 @@ class SubscriptionRegistry {
const opt = { ...options };
opt.onStop = () => {
subscriptionReactivity.changed();
if (options.onStop) options.onStop();
if (options?.onStop) options.onStop();
this.registry[subscription] = null;
};
this.registry[subscription] = Meteor.subscribe(subscription, ...params, opt);

View File

@ -456,9 +456,9 @@
"integrity": "sha512-4L7B+woKIWiwOQrCvvpAQeEob3xoq5fVLTyt0YhXVafy2KWBTCcFVo/Y9n0UpDtGso9jN8pMo3SrYa3V6IayVw=="
},
"@fontsource/recursive": {
"version": "4.5.11",
"resolved": "https://registry.npmjs.org/@fontsource/recursive/-/recursive-4.5.11.tgz",
"integrity": "sha512-pAb+Qk1J0BysC6NTaaGDnhoorT4F8ZZKScWRUfqLpXP/wqH/lp9ccUcTxpS/pMawvmjZyNoKvQucvOvZNscUgw=="
"version": "4.5.12",
"resolved": "https://registry.npmjs.org/@fontsource/recursive/-/recursive-4.5.12.tgz",
"integrity": "sha512-cQxQ73p9GLolyiT3DXdFakrm5pSDZuctjVrODjFTTHu16jNgYzfRoMDi+fbCckDz3ur+WN2yXswrMDnZfNpQ6g=="
},
"@fontsource/source-code-pro": {
"version": "4.5.12",
@ -1404,9 +1404,9 @@
"integrity": "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA=="
},
"@tldraw/core": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/@tldraw/core/-/core-1.20.3.tgz",
"integrity": "sha512-R/HqtQOg8yedcN70m75ekdm2u6dAHQpg3uxmzd7/TsdUnbfZc5UMp3TakYlTD6JwSSRV0n2huI4AD8D/NJ5jEw==",
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/@tldraw/core/-/core-1.21.0.tgz",
"integrity": "sha512-3hvYLR/XwpMI9GvHIHGfYNmkiaKlokx8vekx0Eq/o/NAkqGkGwnZcTG5shbetWzwQTTi5F4vWInJqA/BfX+OCA==",
"requires": {
"@tldraw/intersect": "^1.8.0",
"@tldraw/vec": "^1.8.0",
@ -1423,9 +1423,9 @@
}
},
"@tldraw/tldraw": {
"version": "1.26.4",
"resolved": "https://registry.npmjs.org/@tldraw/tldraw/-/tldraw-1.26.4.tgz",
"integrity": "sha512-hD1lyM+TvaWkAtDKRXHu3Vt2a7OhJOlyEeaNoe0Nhgxb8BbrcdS8sTPKYL4CWjKmdFjYJ2ZfPlaiF2Dzvwk3JA==",
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/@tldraw/tldraw/-/tldraw-1.27.0.tgz",
"integrity": "sha512-ukN1EzLJxDutoo3ZOPZEfEgKUOt+St5yO8P/HSpJF+/QWDEzVrEttZGXOpny02R30tb96rdjtNQEex8TzUjzIQ==",
"requires": {
"@fontsource/caveat-brush": "^4.5.9",
"@fontsource/crimson-pro": "^4.5.10",
@ -1440,7 +1440,7 @@
"@radix-ui/react-popover": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
"@stitches/react": "^1.2.8",
"@tldraw/core": "^1.20.3",
"@tldraw/core": "^1.21.0",
"@tldraw/intersect": "^1.8.0",
"@tldraw/vec": "^1.8.0",
"browser-fs-access": "^0.31.0",
@ -2168,9 +2168,9 @@
}
},
"browser-fs-access": {
"version": "0.31.1",
"resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.31.1.tgz",
"integrity": "sha512-jMz9f56DkLM7LyA8wZYO7CtpoF3RdUk1/FXrnRNybgV0R5eqk/fgFWR0k5IMjPYgK4jmZecytP/UDO5WBi9Dhg=="
"version": "0.31.2",
"resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.31.2.tgz",
"integrity": "sha512-wZSA7UgKMwR6oxddFQeSIoD7cxiNiaZT+iuVJw4/avr9t2ROwu80gxENT0YJChsLxJ7xBbLZDGHTAXfAg3Pq5Q=="
},
"browserslist": {
"version": "4.20.2",
@ -7095,9 +7095,9 @@
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
},
"zustand": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.2.0.tgz",
"integrity": "sha512-eNwaDoD2FYVnMgtNxiMUhTJO780wonZUzJrPQTLYI0erSIMZF8cniWFW22kGQUECd8rdHRJ/ZJL2XO54c9Ttuw==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.2.tgz",
"integrity": "sha512-rd4haDmlwMTVWVqwvgy00ny8rtti/klRoZjFbL/MAcDnmD5qSw/RZc+Vddstdv90M5Lv6RPgWvm1Hivyn0QgJw==",
"requires": {
"use-sync-external-store": "1.2.0"
}

View File

@ -34,8 +34,8 @@
"@jitsi/sdp-interop": "0.1.14",
"@material-ui/core": "^4.12.4",
"@mconf/bbb-diff": "^1.2.0",
"@tldraw/core": "1.20.3",
"@tldraw/tldraw": "1.26.4",
"@tldraw/core": "1.21.0",
"@tldraw/tldraw": "1.27.0",
"autoprefixer": "^10.4.4",
"axios": "^0.21.3",
"babel-runtime": "~6.26.0",

View File

@ -33,6 +33,7 @@ public:
copyright: '©2023 BigBlueButton Inc.'
html5ClientBuild: HTML5_CLIENT_VERSION
helpLink: https://bigbluebutton.org/html5/
delayForUnmountOfSharedNote: 120000
bbbTabletApp:
enabled: false
iosAppStoreUrl: 'https://apps.apple.com/us/app/bigbluebutton-tablet/id1641156756'
@ -121,6 +122,10 @@ public:
# can generate excessive overhead to the server. We recommend
# this value to be kept under 16.
breakouts:
setAllowUserToChooseABreakout: false
setCaptureBreakoutWhiteboard: false
setCaptureBreakoutNotes: false
breakoutRoomLimit: 16
sendInvitationToIncludedModerators: false
# https://github.com/bigbluebutton/bigbluebutton/pull/10826
@ -744,6 +749,11 @@ public:
start: DRAW_START
update: DRAW_UPDATE
end: DRAW_END
styles:
text:
# Initial font family.
# family: mono|sans|script|serif
family: script
toolbar:
multiUserPenOnly: false
colors:

View File

@ -266,6 +266,7 @@
"app.presentationUploder.rejectedError": "The selected file(s) have been rejected. Please check the file type(s).",
"app.presentationUploder.connectionClosedError": "Interrupted by poor connectivity. Please try again.",
"app.presentationUploder.upload.progress": "Uploading ({0}%)",
"app.presentationUploder.conversion.204": "No content to capture",
"app.presentationUploder.upload.413": "File is too large, exceeded the maximum of {0} MB",
"app.presentationUploder.genericError": "Oops, Something went wrong ...",
"app.presentationUploder.upload.408": "Request upload token timeout.",
@ -512,10 +513,10 @@
"app.submenu.application.localeDropdown.te": "Telugu",
"app.submenu.application.localeDropdown.th": "Thai",
"app.submenu.application.localeDropdown.tr": "Turkish",
"app.submenu.application.localeDropdown.tr-TR": "Turkish",
"app.submenu.application.localeDropdown.tr-TR": "Turkish (Turkey)",
"app.submenu.application.localeDropdown.uk-UA": "Ukrainian",
"app.submenu.application.localeDropdown.vi": "Vietnamese",
"app.submenu.application.localeDropdown.vi-VN": "Vietnamese",
"app.submenu.application.localeDropdown.vi-VN": "Vietnamese (Vietnam)",
"app.submenu.application.localeDropdown.zh-CN": "Chinese Simplified (China)",
"app.submenu.application.localeDropdown.zh-TW": "Chinese Traditional (Taiwan)",
"app.submenu.notification.SectionTitle": "Notifications",

View File

@ -246,6 +246,8 @@
"app.presentationUploader.sent": "Enviado",
"app.presentationUploader.exportingTimeout": "La exportación está tardando demasiado...",
"app.presentationUploader.export": "Enviar al chat",
"app.presentationUploader.export.linkAvailable": "Enlace para descargar {0} disponible en el chat público.",
"app.presentationUploader.export.notAccessibleWarning": "puede no ser accesible",
"app.presentationUploader.currentPresentationLabel": "Presentación actual",
"app.presentationUploder.extraHint": "IMPORTANTE: cada fichero no puede exceder {0} MB y {1} páginas",
"app.presentationUploder.uploadLabel": "Cargar",
@ -264,6 +266,7 @@
"app.presentationUploder.rejectedError": "El(los) archivo(s) seleccionado(s) ha(n) sido rechazado(s). Por favor, revise el(los) tipo(s) de archivo.",
"app.presentationUploder.connectionClosedError": "Interrumpido debido a deficiente conectividad. Por favor, inténtalo de nuevo.",
"app.presentationUploder.upload.progress": "Cargando ({0}%)",
"app.presentationUploder.conversion.204": "No hay contenido para capturar",
"app.presentationUploder.upload.413": "El archivo es demasiado grande, excede el máximo de {0} MB",
"app.presentationUploder.genericError": "¡Vaya!, algo ha salido mal...",
"app.presentationUploder.upload.408": "La solicitud de carga del token está fuera de tiempo.",
@ -454,6 +457,68 @@
"app.submenu.application.paginationEnabledLabel": "Paginación de vídeo",
"app.submenu.application.layoutOptionLabel": "Tipo de diseño",
"app.submenu.application.pushLayoutLabel": "Forzar diseño",
"app.submenu.application.localeDropdown.af": "Afrikaans",
"app.submenu.application.localeDropdown.ar": "Árabe",
"app.submenu.application.localeDropdown.az": "Azerbaiyano",
"app.submenu.application.localeDropdown.bg-BG": "Búlgaro",
"app.submenu.application.localeDropdown.bn": "Bengalí",
"app.submenu.application.localeDropdown.ca": "Catalán",
"app.submenu.application.localeDropdown.cs-CZ": "Checo",
"app.submenu.application.localeDropdown.da": "Danés",
"app.submenu.application.localeDropdown.de": "Alemán",
"app.submenu.application.localeDropdown.dv": "Dhivehi",
"app.submenu.application.localeDropdown.el-GR": "Griego (Grecia)",
"app.submenu.application.localeDropdown.en": "Inglés",
"app.submenu.application.localeDropdown.eo": "Esperanto",
"app.submenu.application.localeDropdown.es": "Español",
"app.submenu.application.localeDropdown.es-419": "Español (América Latina)",
"app.submenu.application.localeDropdown.es-ES": "Español (España)",
"app.submenu.application.localeDropdown.es-MX": "Español (México)",
"app.submenu.application.localeDropdown.et": "Estonio",
"app.submenu.application.localeDropdown.eu": "Vasco",
"app.submenu.application.localeDropdown.fa-IR": "Persa",
"app.submenu.application.localeDropdown.fi": "Finlandés",
"app.submenu.application.localeDropdown.fr": "Francés",
"app.submenu.application.localeDropdown.gl": "Gallego",
"app.submenu.application.localeDropdown.he": "Hebreo",
"app.submenu.application.localeDropdown.hi-IN": "Hindi",
"app.submenu.application.localeDropdown.hr": "Croata",
"app.submenu.application.localeDropdown.hu-HU": "Húngaro",
"app.submenu.application.localeDropdown.hy": "Armenio",
"app.submenu.application.localeDropdown.id": "Indonesio",
"app.submenu.application.localeDropdown.it-IT": "Italiano",
"app.submenu.application.localeDropdown.ja": "Japonés",
"app.submenu.application.localeDropdown.ka": "Georgiano",
"app.submenu.application.localeDropdown.km": "Jémer",
"app.submenu.application.localeDropdown.kn": "Canarés",
"app.submenu.application.localeDropdown.ko-KR": "Coreano (Corea)",
"app.submenu.application.localeDropdown.lo-LA": "Laosiano",
"app.submenu.application.localeDropdown.lt-LT": "Lituano",
"app.submenu.application.localeDropdown.lv": "Letón",
"app.submenu.application.localeDropdown.ml": "Malabar",
"app.submenu.application.localeDropdown.mn-MN": "Mongol",
"app.submenu.application.localeDropdown.nb-NO": "Noruego (bokmal)",
"app.submenu.application.localeDropdown.nl": "Holandés",
"app.submenu.application.localeDropdown.oc": "Occitano",
"app.submenu.application.localeDropdown.pl-PL": "Polaco",
"app.submenu.application.localeDropdown.pt": "Portugués",
"app.submenu.application.localeDropdown.pt-BR": "Portugués (Brasil)",
"app.submenu.application.localeDropdown.ro-RO": "Rumano",
"app.submenu.application.localeDropdown.ru": "Ruso",
"app.submenu.application.localeDropdown.sk-SK": "Eslovaco (Eslovaquia)",
"app.submenu.application.localeDropdown.sl": "Esloveno",
"app.submenu.application.localeDropdown.sr": "Serbio",
"app.submenu.application.localeDropdown.sv-SE": "Sueco",
"app.submenu.application.localeDropdown.ta": "Tamil",
"app.submenu.application.localeDropdown.te": "Télugu",
"app.submenu.application.localeDropdown.th": "Tailandés",
"app.submenu.application.localeDropdown.tr": "Turco",
"app.submenu.application.localeDropdown.tr-TR": "Turco (Turquía)",
"app.submenu.application.localeDropdown.uk-UA": "Ucraniano",
"app.submenu.application.localeDropdown.vi": "Vietnamita",
"app.submenu.application.localeDropdown.vi-VN": "Vietnamita (Vietnam)",
"app.submenu.application.localeDropdown.zh-CN": "Chino simplificado (China)",
"app.submenu.application.localeDropdown.zh-TW": "Chino tradicional (Taiwán)",
"app.submenu.notification.SectionTitle": "Notificaciones",
"app.submenu.notification.Desc": "Defina cómo y de qué será notificado.",
"app.submenu.notification.audioAlertLabel": "Alertas audibles",

View File

@ -246,6 +246,8 @@
"app.presentationUploader.sent": "Enviado",
"app.presentationUploader.exportingTimeout": "La exportación está tardando demasiado...",
"app.presentationUploader.export": "Enviar al chat",
"app.presentationUploader.export.linkAvailable": "Enlace para descargar {0} disponible en el chat público.",
"app.presentationUploader.export.notAccessibleWarning": "puede no ser accesible",
"app.presentationUploader.currentPresentationLabel": "Presentación actual",
"app.presentationUploder.extraHint": "IMPORTANTE: cada fichero no puede exceder {0} MB y {1} páginas",
"app.presentationUploder.uploadLabel": "Cargar",
@ -264,6 +266,7 @@
"app.presentationUploder.rejectedError": "El(los) archivo(s) seleccionado(s) ha(n) sido rechazado(s). Por favor, revise el(los) tipo(s) de archivo(s).",
"app.presentationUploder.connectionClosedError": "Interrumpido debido a deficiente conectividad. Por favor, inténtalo de nuevo.",
"app.presentationUploder.upload.progress": "Cargando ({0}%)",
"app.presentationUploder.conversion.204": "No hay contenido para capturar",
"app.presentationUploder.upload.413": "El archivo es demasiado grande, excede el máximo de {0} MB",
"app.presentationUploder.genericError": "Oops, algo salió mal...",
"app.presentationUploder.upload.408": "El tiempo para solicitar el token de subida ha expirado.",
@ -454,6 +457,68 @@
"app.submenu.application.paginationEnabledLabel": "Paginación de vídeo",
"app.submenu.application.layoutOptionLabel": "Tipo de diseño",
"app.submenu.application.pushLayoutLabel": "Forzar diseño",
"app.submenu.application.localeDropdown.af": "Afrikaans",
"app.submenu.application.localeDropdown.ar": "Árabe",
"app.submenu.application.localeDropdown.az": "Azerbaiyano",
"app.submenu.application.localeDropdown.bg-BG": "Búlgaro",
"app.submenu.application.localeDropdown.bn": "Bengalí",
"app.submenu.application.localeDropdown.ca": "Catalán",
"app.submenu.application.localeDropdown.cs-CZ": "Checo",
"app.submenu.application.localeDropdown.da": "Danés",
"app.submenu.application.localeDropdown.de": "Alemán",
"app.submenu.application.localeDropdown.dv": "Dhivehi",
"app.submenu.application.localeDropdown.el-GR": "Griego (Grecia)",
"app.submenu.application.localeDropdown.en": "Inglés",
"app.submenu.application.localeDropdown.eo": "Esperanto",
"app.submenu.application.localeDropdown.es": "Español",
"app.submenu.application.localeDropdown.es-419": "Español (América Latina)",
"app.submenu.application.localeDropdown.es-ES": "Español (España)",
"app.submenu.application.localeDropdown.es-MX": "Español (México)",
"app.submenu.application.localeDropdown.et": "Estonio",
"app.submenu.application.localeDropdown.eu": "Vasco",
"app.submenu.application.localeDropdown.fa-IR": "Persa",
"app.submenu.application.localeDropdown.fi": "Finlandés",
"app.submenu.application.localeDropdown.fr": "Francés",
"app.submenu.application.localeDropdown.gl": "Gallego",
"app.submenu.application.localeDropdown.he": "Hebreo",
"app.submenu.application.localeDropdown.hi-IN": "Hindi",
"app.submenu.application.localeDropdown.hr": "Croata",
"app.submenu.application.localeDropdown.hu-HU": "Húngaro",
"app.submenu.application.localeDropdown.hy": "Armenio",
"app.submenu.application.localeDropdown.id": "Indonesio",
"app.submenu.application.localeDropdown.it-IT": "Italiano",
"app.submenu.application.localeDropdown.ja": "Japonés",
"app.submenu.application.localeDropdown.ka": "Georgiano",
"app.submenu.application.localeDropdown.km": "Jémer",
"app.submenu.application.localeDropdown.kn": "Canarés",
"app.submenu.application.localeDropdown.ko-KR": "Coreano (Corea)",
"app.submenu.application.localeDropdown.lo-LA": "Laosiano",
"app.submenu.application.localeDropdown.lt-LT": "Lituano",
"app.submenu.application.localeDropdown.lv": "Letón",
"app.submenu.application.localeDropdown.ml": "Malabar",
"app.submenu.application.localeDropdown.mn-MN": "Mongol",
"app.submenu.application.localeDropdown.nb-NO": "Noruego (bokmal)",
"app.submenu.application.localeDropdown.nl": "Holandés",
"app.submenu.application.localeDropdown.oc": "Occitano",
"app.submenu.application.localeDropdown.pl-PL": "Polaco",
"app.submenu.application.localeDropdown.pt": "Portugués",
"app.submenu.application.localeDropdown.pt-BR": "Portugués (Brasil)",
"app.submenu.application.localeDropdown.ro-RO": "Rumano",
"app.submenu.application.localeDropdown.ru": "Ruso",
"app.submenu.application.localeDropdown.sk-SK": "Eslovaco (Eslovaquia)",
"app.submenu.application.localeDropdown.sl": "Esloveno",
"app.submenu.application.localeDropdown.sr": "Serbio",
"app.submenu.application.localeDropdown.sv-SE": "Sueco",
"app.submenu.application.localeDropdown.ta": "Tamil",
"app.submenu.application.localeDropdown.te": "Télugu",
"app.submenu.application.localeDropdown.th": "Tailandés",
"app.submenu.application.localeDropdown.tr": "Turco",
"app.submenu.application.localeDropdown.tr-TR": "Turco (Turquía)",
"app.submenu.application.localeDropdown.uk-UA": "Ucraniano",
"app.submenu.application.localeDropdown.vi": "Vietnamita",
"app.submenu.application.localeDropdown.vi-VN": "Vietnamita (Vietnam)",
"app.submenu.application.localeDropdown.zh-CN": "Chino simplificado (China)",
"app.submenu.application.localeDropdown.zh-TW": "Chino tradicional (Taiwán)",
"app.submenu.notification.SectionTitle": "Notificaciones",
"app.submenu.notification.Desc": "Define cómo y qué quieres que te sea notificado.",
"app.submenu.notification.audioAlertLabel": "Alertas sonoras",

View File

@ -2,10 +2,11 @@
"app.home.greeting": "Votre présentation commencera dans quelques instants...",
"app.chat.submitLabel": "Envoyer le message",
"app.chat.loading": "Messages de discussion chargés: {0}%",
"app.chat.errorMaxMessageLength": "Le message est trop long de {0} caractère(s)",
"app.chat.errorMaxMessageLength": "Le message est trop long, il excède le maximum de {0} caractères. ",
"app.chat.disconnected": "Vous êtes déconnecté, les messages ne peuvent pas être envoyés",
"app.chat.locked": "La discussion est verrouillée, les messages ne peuvent pas être envoyés",
"app.chat.inputLabel": "Saisie des messages pour la discussion {0}",
"app.chat.emojiButtonLabel": "Sélecteur d'émoticônes",
"app.chat.inputPlaceholder": "Message {0}",
"app.chat.titlePublic": "Discussion publique",
"app.chat.titlePrivate": "Discussion privée avec {0}",
@ -20,7 +21,8 @@
"app.chat.label": "Discussion",
"app.chat.offline": "Déconnecté",
"app.chat.pollResult": "Résultats du sondage",
"app.chat.breakoutDurationUpdated": "Le temps restant à la réunion privée est maintenant de {0} minutes",
"app.chat.breakoutDurationUpdated": "Le temps restant aux groupes de travail est désormais de {0} minutes",
"app.chat.breakoutDurationUpdatedModerator": "Le temps en groupe de travail est de {0} minutes; une notification a été envoyée",
"app.chat.emptyLogLabel": "Journal de discussion vide",
"app.chat.clearPublicChatMessage": "L'historique des discussions publiques a été effacé par un modérateur",
"app.chat.multi.typing": "Plusieurs utilisateurs sont en train d'écrire",
@ -28,11 +30,32 @@
"app.chat.two.typing": "{0} et {1} sont en train d'écrire",
"app.chat.copySuccess": "Historique de discussion copié",
"app.chat.copyErr": "La copie de l'historique de discussion a échouée",
"app.emojiPicker.search": "Rechercher",
"app.emojiPicker.notFound": "Aucun émoticône trouvé",
"app.emojiPicker.skintext": "Choisissez une teinte par défaut",
"app.emojiPicker.clear": "Clair",
"app.emojiPicker.categories.label": "Catégories d'émoticônes",
"app.emojiPicker.categories.people": "Personnages et corps",
"app.emojiPicker.categories.nature": "Nature et animaux",
"app.emojiPicker.categories.foods": "Nourriture et boisson",
"app.emojiPicker.categories.places": "Lieux et voyage",
"app.emojiPicker.categories.activity": "Activités",
"app.emojiPicker.categories.objects": "Objets",
"app.emojiPicker.categories.symbols": "Symboles",
"app.emojiPicker.categories.flags": "Drapeaux",
"app.emojiPicker.categories.recent": "Utilisés fréquemment",
"app.emojiPicker.categories.search": "Rechercher des résultats",
"app.emojiPicker.skintones.1": "Teinte par défaut",
"app.emojiPicker.skintones.2": "Teinte claire",
"app.emojiPicker.skintones.3": "Ton mat-clair",
"app.emojiPicker.skintones.4": "Ton mat",
"app.emojiPicker.skintones.5": "Ton mat-foncé",
"app.emojiPicker.skintones.6": "Ton foncé",
"app.captions.label": "Sous-titre",
"app.captions.menu.close": "Fermer",
"app.captions.menu.start": "Démarrer",
"app.captions.menu.ariaStart": "Démarrer l'écriture des sous-titres",
"app.captions.menu.ariaStartDesc": "Ouvre l'éditeur de sous-titres et ferme la fenêtre modale",
"app.captions.menu.ariaStartDesc": "Ouvre l'éditeur de sous-titres et ferme la fenêtre de dialogue",
"app.captions.menu.select": "Sélectionnez une langue disponible",
"app.captions.menu.ariaSelect": "Langue des sous-titres",
"app.captions.menu.subtitle": "Veuillez sélectionner une langue et les styles pour les sous-titres de votre réunion.",
@ -53,12 +76,21 @@
"app.captions.speech.start": "Reconnaissance vocale démarrée",
"app.captions.speech.stop": "Reconnaissance vocale arrêtée",
"app.captions.speech.error": "Reconnaissance vocale arrêtée en raison d'une incompatibilité du navigateur ou d'une période de silence",
"app.confirmation.skipConfirm": "Ne plus demander",
"app.confirmation.virtualBackground.title": "Préparer un nouvel arrière-plan virtuel",
"app.confirmation.virtualBackground.description": "{0} sera ajouté comme arrière-plan virtuel. Poursuivre?",
"app.confirmationModal.yesLabel": "Oui",
"app.textInput.sendLabel": "Envoyer",
"app.title.defaultViewLabel": "Vue par défaut de la présentation",
"app.notes.title": "Notes partagées",
"app.notes.label": "Notes",
"app.notes.hide": "Masquer les notes",
"app.notes.locked": "Verrouillé",
"app.notes.disabled": "Épinglé dans l'espace média",
"app.notes.notesDropdown.covertAndUpload": "Retranscrire les notes en présentation à l'écran",
"app.notes.notesDropdown.pinNotes": "Épingler les notes sur le tableau blanc",
"app.notes.notesDropdown.unpinNotes": "Désépingler les notes",
"app.notes.notesDropdown.notesOptions": "Options des notes",
"app.pads.hint": "Appuyez sur Echap pour sélectionner la barre d'outils de l'éditeur",
"app.user.activityCheck": "Vérification de l'activité de l'utilisateur",
"app.user.activityCheck.label": "Vérifier si l'utilisateur est toujours en réunion ({0})",
@ -160,8 +192,8 @@
"app.meeting.endedMessage": "Vous serez redirigé vers l'écran d'accueil",
"app.meeting.alertMeetingEndsUnderMinutesSingular": "La réunion se terminera dans une minute.",
"app.meeting.alertMeetingEndsUnderMinutesPlural": "La conférence se fermera dans {0} minutes.",
"app.meeting.alertBreakoutEndsUnderMinutesPlural": "La réunion privée se fermera dans {0} minutes.",
"app.meeting.alertBreakoutEndsUnderMinutesSingular": "La réunion privée se fermera dans une minute.",
"app.meeting.alertBreakoutEndsUnderMinutesPlural": "Les sessions en groupes de travail se terminent dans {0} minutes.",
"app.meeting.alertBreakoutEndsUnderMinutesSingular": "Les sessions en groupes de travail se terminent dans une minute.",
"app.presentation.hide": "Masquer la présentation",
"app.presentation.notificationLabel": "Présentation courante",
"app.presentation.downloadLabel": "Télécharger",
@ -170,7 +202,7 @@
"app.presentation.endSlideContent": "Fin du contenu de la diapositive",
"app.presentation.changedSlideContent": "La présentation est désormais à la diapositive : {0}",
"app.presentation.emptySlideContent": "Pas de contenu pour la diapositive actuelle",
"app.presentation.options.fullscreen": "Plein écran",
"app.presentation.options.fullscreen": "Présentation en plein écran",
"app.presentation.options.exitFullscreen": "Sortir du plein écran",
"app.presentation.options.minimize": "Réduire",
"app.presentation.options.snapshot": "Capture de la présentation courante",
@ -203,17 +235,32 @@
"app.presentation.presentationToolbar.goToSlide": "Diapositive {0}",
"app.presentation.placeholder": "Il n'y a pas de présentation active actuellement",
"app.presentationUploder.title": "Présentation",
"app.presentationUploder.message": "En tant que présentateur, vous avez la possibilité de télécharger n'importe quel document Office ou fichier PDF. Nous recommandons le fichier PDF pour de meilleurs résultats. Veuillez vous assurer qu'une présentation est sélectionnée à l'aide du cercle à cocher sur la droite.",
"app.presentationUploder.message": "En tant que présentateur, vous avez la possibilité de télécharger n'importe quel document Office ou fichier PDF. Nous recommandons le fichier PDF pour de meilleurs résultats. Veuillez vous assurer qu'une présentation est sélectionnée à l'aide du cercle à cocher sur la gauche.",
"app.presentationUploader.exportHint": "En sélectionnant \"Envoyer dans la discussion\", les utilisateurs disposeront d'un lien de téléchargement de la discussion publique.",
"app.presentationUploader.exportToastHeader": "Envoi dans la discussion de ({0} item)",
"app.presentationUploader.exportToastHeaderPlural": "Envoi dans la discussion de ({0} item)",
"app.presentationUploader.exporting": "Envoi dans la discussion",
"app.presentationUploader.sending": "En cours d'envoi...",
"app.presentationUploader.collecting": "Extraction de la diapositive {0} sur {1}...",
"app.presentationUploader.processing": "Annotation de la diapositive {0} sur {1}...",
"app.presentationUploader.sent": "Envoyé",
"app.presentationUploader.exportingTimeout": "L'export dure trop longtemps...",
"app.presentationUploader.export": "Envoi dans la discussion",
"app.presentationUploader.export.linkAvailable": "Le lien de téléchargement {0} est disponible dans la discussion publique",
"app.presentationUploader.export.notAccessibleWarning": "peut ne pas être accessible",
"app.presentationUploader.currentPresentationLabel": "Présentation courante",
"app.presentationUploder.extraHint": "IMPORTANT: Le volume d'un fichier ne doit pas excéder {0} MB et {1} pages.",
"app.presentationUploder.uploadLabel": "Télécharger",
"app.presentationUploder.confirmLabel": "Confirmer",
"app.presentationUploder.confirmDesc": "Enregistrez vos modifications et lancez la présentation",
"app.presentationUploder.dismissLabel": "Annuler",
"app.presentationUploder.dismissDesc": "Ferme la fenêtre d'option et supprime vos modifications",
"app.presentationUploder.dismissDesc": "Ferme la fenêtre de dialogue et supprime vos modifications",
"app.presentationUploder.dropzoneLabel": "Faites glisser les fichiers ici pour les charger",
"app.presentationUploder.dropzoneImagesLabel": "Faites glisser les images ici pour les charger",
"app.presentationUploder.browseFilesLabel": "ou recherchez dans vos fichiers",
"app.presentationUploder.browseImagesLabel": "ou recherchez/capturez des images",
"app.presentationUploder.externalUploadTitle": "Ajouter du contenu depuis une application tierce",
"app.presentationUploder.externalUploadLabel": "Parcourir les fichiers",
"app.presentationUploder.fileToUpload": "Prêt à être chargé...",
"app.presentationUploder.currentBadge": "En cours",
"app.presentationUploder.rejectedError": "Le(s) fichier(s) sélectionné(s) a (ont) été rejeté()s. Veuillez vérifier leur format.",
@ -230,14 +277,14 @@
"app.presentationUploder.conversion.generatedSlides": "Diapositives générées...",
"app.presentationUploder.conversion.generatingSvg": "Création des images SVG en cours...",
"app.presentationUploder.conversion.pageCountExceeded": "Le nombre de pages dépasse le maximum de {0}",
"app.presentationUploder.conversion.invalidMimeType": "Mauvais format du fichier détecté (extension={0}, type de contenu={1})",
"app.presentationUploder.conversion.conversionTimeout": "La diapositive {0} n'a pas pu être traitée au cours des {1} essais",
"app.presentationUploder.conversion.officeDocConversionInvalid": "Échec du traitement du document Office. Veuillez télécharger un PDF à la place.",
"app.presentationUploder.conversion.officeDocConversionFailed": "Échec du traitement du document office. Veuillez télécharger un PDF à la place.",
"app.presentationUploder.conversion.pdfHasBigPage": "Nous n'avons pas pu convertir le fichier PDF, veuillez essayer de l'optimiser. Taille de page maximum {0}",
"app.presentationUploder.conversion.timeout": "Oups, la conversion a pris trop de temps",
"app.presentationUploder.conversion.pageCountFailed": "Impossible de déterminer le nombre de pages.",
"app.presentationUploder.conversion.unsupportedDocument": "Extension de fichier non prise en charge",
"app.presentationUploder.isDownloadableLabel": "Le téléchargement de la présentation n'est pas autorisé - cliquez pour autoriser le téléchargement",
"app.presentationUploder.isNotDownloadableLabel": "Le téléchargement de la présentation est autorisé - cliquez pour ne plus autoriser le téléchargement",
"app.presentationUploder.removePresentationLabel": "Supprimer la présentation",
"app.presentationUploder.setAsCurrentPresentation": "Définir la présentation comme celle en cours",
"app.presentationUploder.tableHeading.filename": "Nom de fichier",
@ -251,6 +298,10 @@
"app.presentationUploder.clearErrors": "Effacer les erreurs",
"app.presentationUploder.clearErrorsDesc": "Efface les échecs de téléversement des présentations",
"app.presentationUploder.uploadViewTitle": "Charger la présentation",
"app.poll.questionAndoptions.label" : "Intitulé de la question à afficher\nA. Réponse *\nB. Réponse (optionnelle)\nC. Réponse (optionnelle)\nD. Réponse (optionnelle)\nE. Réponse (optionnelle)",
"app.poll.customInput.label": "Saisie personnalisée",
"app.poll.customInputInstructions.label": "Saisie personnalisée est activée - Ecrivez la question et la (les) réponse(s) dans le format proposé ou glissez-déposez un fichier texte dans le même format. ",
"app.poll.maxOptionsWarning.label": "Seules le 5 premières réponses peuvent être utilisées!",
"app.poll.pollPaneTitle": "Sondage",
"app.poll.enableMultipleResponseLabel": "Permettre aux participants de répondre plusieurs fois ?",
"app.poll.quickPollTitle": "Sondage rapide",
@ -270,7 +321,7 @@
"app.poll.clickHereToSelect": "Cliquez ici pour sélectionner",
"app.poll.question.label" : "Écrivez votre question...",
"app.poll.optionalQuestion.label" : "Écrivez votre question (optionnel)...",
"app.poll.userResponse.label" : "Réponse utilisateur",
"app.poll.userResponse.label" : "Réponse saisie",
"app.poll.responseTypes.label" : "Types de réponses",
"app.poll.optionDelete.label" : "Supprimer",
"app.poll.responseChoices.label" : "Choix de réponses",
@ -329,10 +380,10 @@
"app.muteWarning.disableMessage": "Les alertes de micro éteint sont désactivées jusqu'à la réactivation du micro.",
"app.muteWarning.tooltip": "Cliquez pour fermer et désactiver les alertes jusqu'à la prochaine activation du micro",
"app.navBar.settingsDropdown.optionsLabel": "Options",
"app.navBar.settingsDropdown.fullscreenLabel": "Plein écran",
"app.navBar.settingsDropdown.fullscreenLabel": "Application en plein écran",
"app.navBar.settingsDropdown.settingsLabel": "Paramètres",
"app.navBar.settingsDropdown.aboutLabel": "À propos",
"app.navBar.settingsDropdown.leaveSessionLabel": "Quitter la conférence",
"app.navBar.settingsDropdown.leaveSessionLabel": "Quitter la réunion",
"app.navBar.settingsDropdown.exitFullscreenLabel": "Quitter le plein écran",
"app.navBar.settingsDropdown.fullscreenDesc": "Passer le menu de paramétrage en plein écran",
"app.navBar.settingsDropdown.settingsDesc": "Modifier les paramètres généraux",
@ -342,6 +393,7 @@
"app.navBar.settingsDropdown.hotkeysLabel": "Raccourcis clavier",
"app.navBar.settingsDropdown.hotkeysDesc": "Liste des raccourcis clavier disponibles",
"app.navBar.settingsDropdown.helpLabel": "Aide",
"app.navBar.settingsDropdown.openAppLabel": "Ouvrir dans l'application mobile de BigBlueButton ",
"app.navBar.settingsDropdown.helpDesc": "Renvoie l'utilisateur vers des tutoriels vidéos (ouvre un nouvel onglet)",
"app.navBar.settingsDropdown.endMeetingDesc": "Interrompt la réunion en cours",
"app.navBar.settingsDropdown.endMeetingLabel": "Mettre fin à la réunion",
@ -359,7 +411,7 @@
"app.endMeeting.description": "Cette action mettra fin à la séance pour {0} utilisateurs(s) actif(s). Êtes-vous sûr de vouloir mettre fin à cette séance ?",
"app.endMeeting.noUserDescription": "Êtes-vous sûr de vouloir mettre fin à la séance ?",
"app.endMeeting.contentWarning": "Les messages de discussion, les notes partagées, le contenu du tableau blanc et les documents partagés lors de cette séance ne seront plus accessibles directement ",
"app.endMeeting.yesLabel": "Oui",
"app.endMeeting.yesLabel": "Mettre fin à la session pour tous les utilisateurs",
"app.endMeeting.noLabel": "Non",
"app.about.title": "À propos",
"app.about.version": "Version du client :",
@ -369,6 +421,15 @@
"app.about.confirmDesc": "OK",
"app.about.dismissLabel": "Annuler",
"app.about.dismissDesc": "Fermer l'information client",
"app.mobileAppModal.title": "Ouvrir l'application mobile de BigBlueButton",
"app.mobileAppModal.description": "L'application mobile de BigBlueButton est-elle installée sur votre appareil ?",
"app.mobileAppModal.openApp": "Oui, ouvrir l'application maintenant",
"app.mobileAppModal.obtainUrlMsg": "Récupération de l'adresse de la réunion",
"app.mobileAppModal.obtainUrlErrorMsg": "Une erreur est survenue lors de la récupération de l'adresse de la réunion",
"app.mobileAppModal.openStore": "Non, télécharger l'application",
"app.mobileAppModal.dismissLabel": "Annuler",
"app.mobileAppModal.dismissDesc": "Fermer",
"app.mobileAppModal.userConnectedWithSameId": "L'utilisateur {0} vient de se connecter en utilisant le même identifiant que vous",
"app.actionsBar.changeStatusLabel": "Changer de statut",
"app.actionsBar.muteLabel": "Rendre silencieux",
"app.actionsBar.unmuteLabel": "Autoriser à parler",
@ -379,10 +440,12 @@
"app.actionsBar.actionsDropdown.restorePresentationDesc": "Bouton pour rétablir la fenêtre de présentation après qu'elle ait été réduite",
"app.actionsBar.actionsDropdown.minimizePresentationLabel": "Réduire la fenêtre de présentation",
"app.actionsBar.actionsDropdown.minimizePresentationDesc": "Bouton utilisé pour réduire la fenêtre de présentation",
"app.actionsBar.actionsDropdown.layoutModal": "Fenêtre de paramétrage de la mise en page",
"app.screenshare.screenShareLabel" : "Partage d'écran",
"app.submenu.application.applicationSectionTitle": "Application",
"app.submenu.application.animationsLabel": "Animations",
"app.submenu.application.audioFilterLabel": "Filtres audios pour microphone",
"app.submenu.application.darkThemeLabel": "Mode sombre",
"app.submenu.application.fontSizeControlLabel": "Taille des caractères",
"app.submenu.application.increaseFontBtnLabel": "Augmenter la taille des caractères",
"app.submenu.application.decreaseFontBtnLabel": "Diminuer la taille des caractères",
@ -392,6 +455,69 @@
"app.submenu.application.noLocaleOptionLabel": "Aucune langue d'application détectée",
"app.submenu.application.paginationEnabledLabel": "Pagination de la vidéo",
"app.submenu.application.layoutOptionLabel": "Type de mise en page",
"app.submenu.application.pushLayoutLabel": "Appliquer la mise en page",
"app.submenu.application.localeDropdown.af": "Afrikaans",
"app.submenu.application.localeDropdown.ar": "Arabe",
"app.submenu.application.localeDropdown.az": "Azéri",
"app.submenu.application.localeDropdown.bg-BG": "Bulgare",
"app.submenu.application.localeDropdown.bn": "Bengali",
"app.submenu.application.localeDropdown.ca": "Catalan",
"app.submenu.application.localeDropdown.cs-CZ": "Tchèque",
"app.submenu.application.localeDropdown.da": "Danois",
"app.submenu.application.localeDropdown.de": "Allemand",
"app.submenu.application.localeDropdown.dv": "Divehi",
"app.submenu.application.localeDropdown.el-GR": "Grec (Grèce)",
"app.submenu.application.localeDropdown.en": "Anglais",
"app.submenu.application.localeDropdown.eo": "Espéranto",
"app.submenu.application.localeDropdown.es": "Espagnol",
"app.submenu.application.localeDropdown.es-419": "Espagnol (Amérique latine)",
"app.submenu.application.localeDropdown.es-ES": "Espagnol (Espagne)",
"app.submenu.application.localeDropdown.es-MX": "Espagnol (Mexique)",
"app.submenu.application.localeDropdown.et": "Estonien",
"app.submenu.application.localeDropdown.eu": "Basque",
"app.submenu.application.localeDropdown.fa-IR": "Persan",
"app.submenu.application.localeDropdown.fi": "Finnois",
"app.submenu.application.localeDropdown.fr": "Français",
"app.submenu.application.localeDropdown.gl": "Galicien",
"app.submenu.application.localeDropdown.he": "Hébreu",
"app.submenu.application.localeDropdown.hi-IN": "Hindi",
"app.submenu.application.localeDropdown.hr": "Croate",
"app.submenu.application.localeDropdown.hu-HU": "Hongrois",
"app.submenu.application.localeDropdown.hy": "Arménien",
"app.submenu.application.localeDropdown.id": "Indonésien",
"app.submenu.application.localeDropdown.it-IT": "Italien",
"app.submenu.application.localeDropdown.ja": "Japonais",
"app.submenu.application.localeDropdown.ka": "Géorgien",
"app.submenu.application.localeDropdown.km": "Khmer",
"app.submenu.application.localeDropdown.kn": "Kannada",
"app.submenu.application.localeDropdown.ko-KR": "Coréen (Corée)",
"app.submenu.application.localeDropdown.lo-LA": "Lao",
"app.submenu.application.localeDropdown.lt-LT": "Lituanien",
"app.submenu.application.localeDropdown.lv": "Letton",
"app.submenu.application.localeDropdown.ml": "Malayalam",
"app.submenu.application.localeDropdown.mn-MN": "Mongol",
"app.submenu.application.localeDropdown.nb-NO": "Norvégien (bokmal)",
"app.submenu.application.localeDropdown.nl": "Néerlandais",
"app.submenu.application.localeDropdown.oc": "Occitan",
"app.submenu.application.localeDropdown.pl-PL": "Polonais",
"app.submenu.application.localeDropdown.pt": "Portugais",
"app.submenu.application.localeDropdown.pt-BR": "Portugais (Brésil)",
"app.submenu.application.localeDropdown.ro-RO": "Roumain",
"app.submenu.application.localeDropdown.ru": "Russe",
"app.submenu.application.localeDropdown.sk-SK": "Slovaque (Slovaquie)",
"app.submenu.application.localeDropdown.sl": "Slovène",
"app.submenu.application.localeDropdown.sr": "Serbe",
"app.submenu.application.localeDropdown.sv-SE": "Suédois",
"app.submenu.application.localeDropdown.ta": "Tamoul",
"app.submenu.application.localeDropdown.te": "Telugu",
"app.submenu.application.localeDropdown.th": "Thaï",
"app.submenu.application.localeDropdown.tr": "Turc",
"app.submenu.application.localeDropdown.tr-TR": "Turc (Turquie)",
"app.submenu.application.localeDropdown.uk-UA": "Ukrénien",
"app.submenu.application.localeDropdown.vi": "Vietnamien",
"app.submenu.application.localeDropdown.vi-VN": "Vietnamien (Vietnam)",
"app.submenu.application.localeDropdown.zh-CN": "Chinois simplifié (Chine)",
"app.submenu.application.localeDropdown.zh-TW": "Chinois Traditionnel (Taïwan)",
"app.submenu.notification.SectionTitle": "Notifications",
"app.submenu.notification.Desc": "Gestion des notifications",
"app.submenu.notification.audioAlertLabel": "Alerte sonore",
@ -420,7 +546,7 @@
"app.settings.main.save.label.description": "Enregistre les changements et ferme le menu des paramètres",
"app.settings.dataSavingTab.label": "Économies de données",
"app.settings.dataSavingTab.webcam": "Activer les webcams des autres participants",
"app.settings.dataSavingTab.screenShare": "Activer le partage d'écran",
"app.settings.dataSavingTab.screenShare": "Activer le partage d'écran des autres participants",
"app.settings.dataSavingTab.description": "Pour économiser votre bande passante, ajustez l'affichage actuel.",
"app.settings.save-notification.label": "Les paramètres ont été enregistrés",
"app.statusNotifier.lowerHands": "Mains baissées",
@ -440,7 +566,6 @@
"app.actionsBar.actionsDropdown.presentationLabel": "Gérer les documents de présentation",
"app.actionsBar.actionsDropdown.initPollLabel": "Préparer un sondage",
"app.actionsBar.actionsDropdown.desktopShareLabel": "Partager votre écran",
"app.actionsBar.actionsDropdown.lockedDesktopShareLabel": "Partage d'écran verrouillé",
"app.actionsBar.actionsDropdown.stopDesktopShareLabel": "Cesser le partage d'écran",
"app.actionsBar.actionsDropdown.presentationDesc": "Charger votre présentation",
"app.actionsBar.actionsDropdown.initPollDesc": "Préparer un sondage",
@ -449,14 +574,15 @@
"app.actionsBar.actionsDropdown.pollBtnLabel": "Lancer un sondage",
"app.actionsBar.actionsDropdown.pollBtnDesc": "Affiche/cache le volet de sondage",
"app.actionsBar.actionsDropdown.saveUserNames": "Sauvegarder les noms d'utilisateur",
"app.actionsBar.actionsDropdown.createBreakoutRoom": "Créer des réunions privées",
"app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "Créer des réunions privées pour scinder la réunion en cours",
"app.actionsBar.actionsDropdown.createBreakoutRoom": "Créer des salles pour les groupes de travail",
"app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "Créer des groupes de travail pour scinder la réunion en cours",
"app.actionsBar.actionsDropdown.captionsLabel": "Écrire des sous-titres SME",
"app.actionsBar.actionsDropdown.captionsDesc": "Affiche/cache le volet des sous-titres",
"app.actionsBar.actionsDropdown.takePresenter": "Devenir présentateur",
"app.actionsBar.actionsDropdown.takePresenterDesc": "S'assigner comme nouveau présentateur",
"app.actionsBar.actionsDropdown.selectRandUserLabel": "Sélectionner un utilisateur aléatoirement",
"app.actionsBar.actionsDropdown.selectRandUserDesc": "Sélectionne aléatoirement un utilisateur parmi les participants disponibles",
"app.actionsBar.actionsDropdown.propagateLayoutLabel": "Diffuser la mise en page",
"app.actionsBar.emojiMenu.statusTriggerLabel": "Définir votre statut",
"app.actionsBar.emojiMenu.awayLabel": "Éloigné",
"app.actionsBar.emojiMenu.awayDesc": "Passer votre statut à « éloigné »",
@ -496,19 +622,21 @@
"app.audioNotification.audioFailedError1012": "Connexion fermée (erreur ICE 1012)",
"app.audioNotification.audioFailedMessage": "Votre connexion en mode audio a échoué",
"app.audioNotification.mediaFailedMessage": "getUserMicMedia a échoué car seules les origines sécurisées sont autorisées",
"app.audioNotification.deviceChangeFailed": "Le changement d'appareil audio a échoué. Vérifiez que l'appareil est bien installé et disponible.",
"app.audioNotification.closeLabel": "Fermer",
"app.audioNotificaion.reconnectingAsListenOnly": "Le microphone est verrouillé pour les participants, vous êtes connecté en mode écoute uniquement.",
"app.breakoutJoinConfirmation.title": "Rejoindre la réunion privée",
"app.breakoutJoinConfirmation.title": "Rejoindre le groupe de travail",
"app.breakoutJoinConfirmation.message": "Voulez-vous rejoindre la séance",
"app.breakoutJoinConfirmation.confirmDesc": "Rejoignez la réunion privée",
"app.breakoutJoinConfirmation.confirmDesc": "Rejoignez le groupe de travail",
"app.breakoutJoinConfirmation.dismissLabel": "Annuler",
"app.breakoutJoinConfirmation.dismissDesc": "Fermer et refuser d'entrer dans la réunion privée",
"app.breakoutJoinConfirmation.freeJoinMessage": "Choisissez une réunion privée à rejoindre",
"app.breakoutTimeRemainingMessage": "Temps restant dans la réunion privée : {0}",
"app.breakoutWillCloseMessage": "Le temps s'est écoulé. La réunion privée fermera bientôt",
"app.breakoutJoinConfirmation.dismissDesc": "Fermer et refuser de rejoindre le groupe de travail",
"app.breakoutJoinConfirmation.freeJoinMessage": "Choisissez une groupe de travail à rejoindre",
"app.breakoutTimeRemainingMessage": "Temps restant pour le groupe de travail : {0}",
"app.breakoutWillCloseMessage": "Le temps s'est écoulé. La groupe de travail fermera bientôt",
"app.breakout.dropdown.manageDuration": "Changer la durée",
"app.breakout.dropdown.destroyAll": "Clôturer les réunions privées",
"app.breakout.dropdown.options": "Options des réunions privées",
"app.breakout.dropdown.destroyAll": "Clôturer les groupes de travail",
"app.breakout.dropdown.options": "Options des groupes de travail",
"app.breakout.dropdown.manageUsers": "Gestion des utilisateurs",
"app.calculatingBreakoutTimeRemaining": "Calcul du temps restant...",
"app.audioModal.ariaTitle": "Fenêtre modale pour joindre la réunion en audio",
"app.audioModal.microphoneLabel": "Microphone",
@ -555,6 +683,7 @@
"app.audio.changeAudioDevice": "Changer de périphérique audio",
"app.audio.enterSessionLabel": "Rejoindre la séance",
"app.audio.playSoundLabel": "Jouer un son",
"app.audio.stopAudioFeedback": "Arrêter le retour audio",
"app.audio.backLabel": "Retour",
"app.audio.loading": "Chargement en cours",
"app.audio.microphones": "Microphones",
@ -567,10 +696,32 @@
"app.audio.audioSettings.testSpeakerLabel": "Testez votre haut-parleur",
"app.audio.audioSettings.microphoneStreamLabel": "Volume de votre flux audio",
"app.audio.audioSettings.retryLabel": "Réessayer",
"app.audio.audioSettings.fallbackInputLabel": "Entrée audio {0}",
"app.audio.audioSettings.fallbackOutputLabel": "Sortie audio {0}",
"app.audio.audioSettings.defaultOutputDeviceLabel": "Par défaut",
"app.audio.audioSettings.findingDevicesLabel": "Recherche des appareils...",
"app.audio.listenOnly.backLabel": "Retour",
"app.audio.listenOnly.closeLabel": "Fermer",
"app.audio.permissionsOverlay.title": "Autoriser BigBlueButton à utiliser votre micro",
"app.audio.permissionsOverlay.hint": "Il est nécessaire que vous nous autorisiez à utiliser vos appareils multimédias pour que vous puissiez participer à la réunion",
"app.audio.captions.button.start": "Initier un sous titrage SME",
"app.audio.captions.button.stop": "Arrêter le sous titrage SME",
"app.audio.captions.button.language": "Langue",
"app.audio.captions.button.transcription": "Transcription",
"app.audio.captions.button.transcriptionSettings": "Paramètres de la transcription",
"app.audio.captions.speech.title": "Transcription automatique",
"app.audio.captions.speech.disabled": "Desactivé",
"app.audio.captions.speech.unsupported": "Votre navigateur n'est pas compatible avec la reconnaissance vocale. L'entrée audio ne sera pas retranscrite.",
"app.audio.captions.select.de-DE": "Allemand",
"app.audio.captions.select.en-US": "Anglais",
"app.audio.captions.select.es-ES": "Espagnol",
"app.audio.captions.select.fr-FR": "Français",
"app.audio.captions.select.hi-ID": "Hindi",
"app.audio.captions.select.it-IT": "Italien",
"app.audio.captions.select.ja-JP": "Japonais",
"app.audio.captions.select.pt-BR": "Portugais",
"app.audio.captions.select.ru-RU": "Russe",
"app.audio.captions.select.zh-CN": "Chinois",
"app.error.removed": "Vous avez été écarté de la réunion",
"app.error.meeting.ended": "Vous avez été déconnecté de la réunion",
"app.meeting.logout.duplicateUserEjectReason": " Un compte déjà connecté tente de rejoindre la réunion",
@ -578,6 +729,7 @@
"app.meeting.logout.ejectedFromMeeting": "Vous avez été retiré de la réunion",
"app.meeting.logout.validateTokenFailedEjectReason": "Le jeton d'autorisation n'a pas pu être validé",
"app.meeting.logout.userInactivityEjectReason": "Utilisateur trop longtemps inactif ",
"app.meeting.logout.maxParticipantsReached": "Le nombre maximum de participants pour cette réunion a été atteint.",
"app.meeting-ended.rating.legendLabel": "Évaluation",
"app.meeting-ended.rating.starLabel": "Favori",
"app.modal.close": "Fermer",
@ -599,8 +751,11 @@
"app.error.403": "Vous avez été retiré de la réunion",
"app.error.404": "Non trouvé",
"app.error.408": "Échec de l'authentification",
"app.error.409": "Conflit",
"app.error.410": "La conférence est terminée",
"app.error.500": "Oups, quelque chose s'est mal passé",
"app.error.503": "Vous avez été déconnecté",
"app.error.disconnected.rejoin": "Vous pouvez rafraichir la page pour réintégrer la réunion.",
"app.error.userLoggedOut": "Le jeton de session est invalide car l'utilisateur est déconnecté",
"app.error.ejectedUser": "Le jeton de session est invalide car l'utilisateur a été expulsé",
"app.error.joinedAnotherWindow": "Il semble que cette conférence est ouverte dans une autre fenêtre de navigateur",
@ -644,14 +799,18 @@
"app.userList.guest.privateMessageLabel": "Message",
"app.userList.guest.acceptLabel": "Accepter",
"app.userList.guest.denyLabel": "Refuser",
"app.userList.guest.feedbackMessage": "Mesure prise:",
"app.user-info.title": "Recherche dans l'annuaire",
"app.toast.breakoutRoomEnded": "La réunion privée s'est terminée. Veuillez rejoindre l'audio.",
"app.toast.breakoutRoomEnded": "La session en groupe de travail est terminée. Veuillez rejoindre l'audio.",
"app.toast.chat.public": "Nouveau message de discussion publique",
"app.toast.chat.private": "Nouveau message de discussion privée",
"app.toast.chat.system": "Système",
"app.toast.chat.poll": "Résultats du sondage",
"app.toast.chat.pollClick": "Les résultats du sondage ont été publiés. Cliquez ici pour les voir.",
"app.toast.clearedEmoji.label": "Emoticône de statut effacé",
"app.toast.setEmoji.label": "Emoticône de statut défini sur {0}",
"app.toast.meetingMuteOn.label": "Tous les utilisateurs ont été rendus silencieux",
"app.toast.meetingMuteOnViewers.label": "Les spectateurs sont en mode silencieux",
"app.toast.meetingMuteOff.label": "Mode silencieux désactivé",
"app.toast.setEmoji.raiseHand": "Vous avez levé la main",
"app.toast.setEmoji.lowerHand": "Votre main a été abaissée",
@ -688,6 +847,38 @@
"app.shortcut-help.toggleFullscreenKey": "Entrée",
"app.shortcut-help.nextSlideKey": "Flèche droite",
"app.shortcut-help.previousSlideKey": "Flèche gauche",
"app.shortcut-help.select": "Choisir un outil",
"app.shortcut-help.pencil": "Crayon",
"app.shortcut-help.eraser": "Gomme",
"app.shortcut-help.rectangle": "Rectangle",
"app.shortcut-help.elipse": "Ellipse",
"app.shortcut-help.triangle": "Triangle",
"app.shortcut-help.line": "Ligne",
"app.shortcut-help.arrow": "Flèche",
"app.shortcut-help.text": "Outil texte",
"app.shortcut-help.note": "Note autocollante",
"app.shortcut-help.general": "Général",
"app.shortcut-help.presentation": "Présentation",
"app.shortcut-help.whiteboard": "Tableau blanc",
"app.shortcut-help.zoomIn": "Zoom avant",
"app.shortcut-help.zoomOut": "Zoom arrière",
"app.shortcut-help.zoomFit": "Réinitialiser le zoom",
"app.shortcut-help.zoomSelect": "Zoomer sur la sélection",
"app.shortcut-help.flipH": "Renversement horizontal",
"app.shortcut-help.flipV": "Renversement vertical",
"app.shortcut-help.lock": "Verrouiller/déverrouiller",
"app.shortcut-help.moveToFront": "Déplacer au premier plan",
"app.shortcut-help.moveToBack": "Déplacer à l'arrière-plan",
"app.shortcut-help.moveForward": "Avancer",
"app.shortcut-help.moveBackward": "Reculer",
"app.shortcut-help.undo": "Annuler l'action",
"app.shortcut-help.redo": "Rétablir l'action",
"app.shortcut-help.cut": "Couper",
"app.shortcut-help.copy": "Coller",
"app.shortcut-help.paste": "Coller",
"app.shortcut-help.selectAll": "Tout sélectionner",
"app.shortcut-help.delete": "Effacer",
"app.shortcut-help.duplicate": "Dupliquer",
"app.lock-viewers.title": "Limiter la communication des participants",
"app.lock-viewers.description": "Ces options vous permettent de restreindre l'utilisation de certaines fonctionnalités par les participants.",
"app.lock-viewers.featuresLable": "Fonctionnalité",
@ -737,13 +928,19 @@
"app.connection-status.next": "Page suivante",
"app.connection-status.prev": "Page précédente",
"app.learning-dashboard.label": "Tableau de bord d'activité des participants",
"app.learning-dashboard.description": "Ouvrir le tableau de bord avec les activités des utilisateurs",
"app.learning-dashboard.description": "Tableau d'activité des utilisateurs",
"app.learning-dashboard.clickHereToOpen": "Ouvrir le tableau de bord d'activité des participants",
"app.recording.startTitle": "Commencer l'enregistrement",
"app.recording.stopTitle": "Enregistrement en pause",
"app.recording.resumeTitle": "Reprendre l'enregistrement",
"app.recording.startDescription": "Vous pouvez à nouveau utiliser le bouton d'enregistrement ultérieurement pour mettre l'enregistrement en pause.",
"app.recording.stopDescription": "Êtes-vous sûr de vouloir mettre l'enregistrement en pause ? Vous pouvez reprendre en utilisant à nouveau le bouton d'enregistrement.",
"app.recording.notify.title": "L'enregistrement à commencé",
"app.recording.notify.description": "La suite de cette session sera enregistrée.",
"app.recording.notify.continue": "Poursuivre",
"app.recording.notify.leave": "Quitter la session",
"app.recording.notify.continueLabel" : "Accepter l'enregistrement et poursuivre",
"app.recording.notify.leaveLabel" : "Refuser l'enregistrement et quitter la réunion",
"app.videoPreview.cameraLabel": "Caméra",
"app.videoPreview.profileLabel": "Qualité",
"app.videoPreview.quality.low": "Bas",
@ -760,13 +957,20 @@
"app.videoPreview.webcamOptionLabel": "Sélectionner une webcam",
"app.videoPreview.webcamPreviewLabel": "Aperçu de la webcam",
"app.videoPreview.webcamSettingsTitle": "Paramètres de la webcam",
"app.videoPreview.webcamEffectsTitle": "Réglages de l'image pour la webcam",
"app.videoPreview.webcamVirtualBackgroundLabel": "Paramètres d'arrière-plan virtuel",
"app.videoPreview.webcamVirtualBackgroundDisabledLabel": "Le périphérique ne prend pas en charge les arrière-plans virtuels",
"app.videoPreview.webcamNotFoundLabel": "Webcam introuvable",
"app.videoPreview.profileNotFoundLabel": "Profil de caméra non supporté",
"app.videoPreview.brightness": "Luminosité",
"app.videoPreview.wholeImageBrightnessLabel": "Image complète",
"app.videoPreview.wholeImageBrightnessDesc": "Applique le réglage de la luminosité à l'image principale et à l'arrière-plan",
"app.videoPreview.sliderDesc": "Réduit ou augmente le luminosité",
"app.video.joinVideo": "Partager webcam",
"app.video.connecting": "Le partage de la webcam démarre...",
"app.video.leaveVideo": "Arrêtez le partage de la webcam",
"app.video.videoSettings": "Réglages vidéo",
"app.video.visualEffects": "Réglages de l'image",
"app.video.advancedVideo": "Ouvrir les paramètres avancés",
"app.video.iceCandidateError": "Erreur lors de l'ajout du candidat ICE",
"app.video.iceConnectionStateError": "Échec de connexion (erreur ICE 1107)",
@ -782,6 +986,7 @@
"app.video.notReadableError": "Impossible d'obtenir la vidéo de la webcam. Assurez-vous qu'aucun autre programme n'utilise la webcam",
"app.video.timeoutError": "Le navigateur n'a pas répondu à temps",
"app.video.genericError": "Une erreur inconnue s'est produite avec l'appareil (Erreur {0})",
"app.video.inactiveError": "Votre webcam s'est brusquement éteinte. Veuillez vérifier les autorisations données au navigateur.",
"app.video.mediaTimedOutError": "Le flux de votre webcam a été interrompu. Essayer de le partager à nouveau",
"app.video.mediaFlowTimeout1020": "Le média n'a pas pu atteindre le serveur (erreur 1020)",
"app.video.suggestWebcamLock": "Appliquer le paramètre de verrouillage aux webcams des participants ?",
@ -804,8 +1009,17 @@
"app.video.virtualBackground.board": "Tableau",
"app.video.virtualBackground.coffeeshop": "Café",
"app.video.virtualBackground.background": "Arrière-plan",
"app.video.virtualBackground.backgroundWithIndex": "Arrière-plan {0}",
"app.video.virtualBackground.custom": "Téléverser depuis votre ordinateur",
"app.video.virtualBackground.remove": "Supprimer l'image qui a été ajoutée",
"app.video.virtualBackground.genericError": "Échec de l'application de l'effet de caméra. Réessayez. ",
"app.video.virtualBackground.camBgAriaDesc": "Défini l'arrière plan virtuel de caméra à {0}",
"app.video.virtualBackground.maximumFileSizeExceeded": "La limite de taille pour un fichier est dépassée. ({0}MB)",
"app.video.virtualBackground.typeNotAllowed": "Ce type de fichier n'est pas autorisé",
"app.video.virtualBackground.errorOnRead": "Une erreur s'est produite lors de la lecture du fichier.",
"app.video.virtualBackground.uploaded": "Chargé",
"app.video.virtualBackground.uploading": "En cours de chargement...",
"app.video.virtualBackground.button.customDesc": "Ajoute un nouveau arrière-plan virtuel",
"app.video.camCapReached": "Vous ne pouvez pas partager plus de caméras",
"app.video.meetingCamCapReached": "La conférence a atteint sa limite de caméras simultanées",
"app.video.dropZoneLabel": "Déposer ici",
@ -826,6 +1040,7 @@
"app.whiteboard.annotations.poll": "Les résultats du sondage ont été publiés",
"app.whiteboard.annotations.pollResult": "Résultats du sondage",
"app.whiteboard.annotations.noResponses": "Pas de réponses",
"app.whiteboard.annotations.notAllowed": "Vous n'êtes pas autorisé à faire ce changement.",
"app.whiteboard.toolbar.tools": "Outils",
"app.whiteboard.toolbar.tools.hand": "Panoramique",
"app.whiteboard.toolbar.tools.pencil": "Crayon",
@ -852,6 +1067,7 @@
"app.whiteboard.toolbar.color.silver": "Argenté",
"app.whiteboard.toolbar.undo": "Annuler l'annotation",
"app.whiteboard.toolbar.clear": "Effacer toutes les annotations",
"app.whiteboard.toolbar.clearConfirmation": "Êtes-vous sûr de vouloir effacer les annotations?",
"app.whiteboard.toolbar.multiUserOn": "Activer le mode multi-utilisateur",
"app.whiteboard.toolbar.multiUserOff": "Désactiver le mode multi-utilisateur",
"app.whiteboard.toolbar.palmRejectionOn": "Contacts de la paume de la main sur les écrans tactiles : Désactivés",
@ -871,19 +1087,19 @@
"app.videoDock.webcamUnfocusDesc": "Arrêter de centrer sur la webcam sélectionnée",
"app.videoDock.webcamPinLabel": "Épingler",
"app.videoDock.webcamPinDesc": "Épingler la caméra sélectionnée",
"app.videoDock.webcamFullscreenLabel": "Webcam en plein écran",
"app.videoDock.webcamSqueezedButtonLabel": "Options de la webcam",
"app.videoDock.webcamUnpinLabel": "Désépingler",
"app.videoDock.webcamUnpinLabelDisabled": "Seuls les modérateurs peuvent désépingler des utilisateurs",
"app.videoDock.webcamUnpinDesc": "Désépingler la caméra sélectionnée",
"app.videoDock.autoplayBlockedDesc": "Nous avons besoin de votre permission pour vous montrer les webcams des autres utilisateurs.",
"app.videoDock.autoplayAllowLabel": "Voir les webcams",
"app.invitation.title": "Invitation à une réunion privée",
"app.invitation.confirm": "Inviter",
"app.createBreakoutRoom.title": "Salle de réunion privée",
"app.createBreakoutRoom.ariaTitle": "Masquer les salles de réunion privées",
"app.createBreakoutRoom.breakoutRoomLabel": "Réunions privées {0}",
"app.createBreakoutRoom.title": "Salles pour les groupes de travail",
"app.createBreakoutRoom.ariaTitle": "Masquer les salles pour les groupes de travail",
"app.createBreakoutRoom.breakoutRoomLabel": "Salles pour les groupes de travail {0}",
"app.createBreakoutRoom.askToJoin": "Demander à rejoindre une réunion",
"app.createBreakoutRoom.generatingURL": "Générer l'URL",
"app.createBreakoutRoom.generatingURLMessage": "Nous sommes en train de générer une URL pour rejoindre le salon du groupe sélectionné. Cela peut prendre quelques secondes...",
"app.createBreakoutRoom.generatingURLMessage": "Nous sommes en train de générer une URL pour rejoindre le groupe de travail sélectionné. Cela peut prendre quelques secondes...",
"app.createBreakoutRoom.duration": "Durée {0}",
"app.createBreakoutRoom.room": "Réunion {0}",
"app.createBreakoutRoom.notAssigned": "Non attribué ({0})",
@ -896,22 +1112,24 @@
"app.createBreakoutRoom.numberOfRooms": "Nombre de réunions",
"app.createBreakoutRoom.durationInMinutes": "Durée (minutes)",
"app.createBreakoutRoom.randomlyAssign": "Affecter aléatoirement",
"app.createBreakoutRoom.randomlyAssignDesc": "Affecte les utilisateurs aux salles de réunion privées de manière aléatoire",
"app.createBreakoutRoom.randomlyAssignDesc": "Affecte les utilisateurs de manière aléatoire dans les groupes de travail",
"app.createBreakoutRoom.resetAssignments": "Réinitialiser les affectations",
"app.createBreakoutRoom.resetAssignmentsDesc": "Réinitialiser toutes les affectations des utilisateurs aux salles",
"app.createBreakoutRoom.endAllBreakouts": "Clore toutes les réunions privées",
"app.createBreakoutRoom.endAllBreakouts": "Clore tous les groupes de travail",
"app.createBreakoutRoom.chatTitleMsgAllRooms": "toutes les salles",
"app.createBreakoutRoom.msgToBreakoutsSent": "Le message a été envoyé à {0} réunions privées",
"app.createBreakoutRoom.msgToBreakoutsSent": "Le message a été envoyé à {0} groupes de travail",
"app.createBreakoutRoom.roomName": "{0} (Réunion - {1})",
"app.createBreakoutRoom.doneLabel": "Terminé",
"app.createBreakoutRoom.nextLabel": "Suivant",
"app.createBreakoutRoom.minusRoomTime": "Diminuer le temps de la réunion privée à",
"app.createBreakoutRoom.addRoomTime": "Augmenter le temps de la réunion privée à",
"app.createBreakoutRoom.minusRoomTime": "Diminuer la durée des groupes de travail à",
"app.createBreakoutRoom.addRoomTime": "Augmenter la durée des groupes de travail à",
"app.createBreakoutRoom.addParticipantLabel": "+ Ajouter un participant",
"app.createBreakoutRoom.freeJoin": "Autoriser les participants à choisir la salle de réunion privée qu'ils souhaitent rejoindre",
"app.createBreakoutRoom.leastOneWarnBreakout": "Vous devez placer au moins un participant dans une réunion privée.",
"app.createBreakoutRoom.minimumDurationWarnBreakout": "La durée minimum d'une réunion privée est de {0} minutes.",
"app.createBreakoutRoom.modalDesc": "Conseil : vous pouvez glisser-déposer le nom d'un utilisateur pour l'affecter à une salle de réunion spécifique.",
"app.createBreakoutRoom.captureNotes": "Faire une capture des notes partagées lors de la fermeture des groupes de travail",
"app.createBreakoutRoom.captureSlides": "Faire une capture du tableau blanc lors de la fermeture des groupes de travail",
"app.createBreakoutRoom.leastOneWarnBreakout": "Vous devez placer au moins un participant dans chaque groupe de travail",
"app.createBreakoutRoom.minimumDurationWarnBreakout": "La durée minimum d'une session en groupes de travail est de {0} minutes.",
"app.createBreakoutRoom.modalDesc": "Conseil : vous pouvez glisser-déposer le nom d'un utilisateur pour l'affecter à un groupe de travail spécifique.",
"app.createBreakoutRoom.roomTime": "{0} minutes",
"app.createBreakoutRoom.numberOfRoomsError": "Le nombre de réunions n'est pas valide.",
"app.createBreakoutRoom.duplicatedRoomNameError": "Le nom du salon ne peut être dupliqué.",
@ -919,8 +1137,16 @@
"app.createBreakoutRoom.setTimeInMinutes": "Fixer la durée à (minutes)",
"app.createBreakoutRoom.setTimeLabel": "Appliquer",
"app.createBreakoutRoom.setTimeCancel": "Annuler",
"app.createBreakoutRoom.setTimeHigherThanMeetingTimeError": "La durée des réunions privées ne peut pas excéder le temps restant de la réunion.",
"app.createBreakoutRoom.roomNameInputDesc": "Met à jour le nom de la salle de réunion privée",
"app.createBreakoutRoom.setTimeHigherThanMeetingTimeError": "La durée des sessions en groupe de travail ne peut pas excéder le temps restant de la réunion.",
"app.createBreakoutRoom.roomNameInputDesc": "Met à jour le nom du groupe de travail",
"app.createBreakoutRoom.movedUserLabel": "Déplacé {0} dans le salon privé {1}",
"app.updateBreakoutRoom.modalDesc": "Pour informer ou inviter un utilisateur, glisser son nom d'utilisateur dans le salon souhaité",
"app.updateBreakoutRoom.cancelLabel": "Annuler",
"app.updateBreakoutRoom.title": "Mettre à jour les groupes de travail",
"app.updateBreakoutRoom.confirm": "Appliquer",
"app.updateBreakoutRoom.userChangeRoomNotification": "Vous avez été déplacé vers la salle {0}",
"app.smartMediaShare.externalVideo": "Vidéo(s) de source externe",
"app.update.resetRoom": "Réinitialiser la salle de l'utilisateur",
"app.externalVideo.start": "Partager une nouvelle vidéo",
"app.externalVideo.title": "Partager une vidéo externe",
"app.externalVideo.input": "URL vidéo externe",
@ -946,14 +1172,23 @@
"app.debugWindow.form.enableAutoarrangeLayoutDescription": "(il sera désactivé si vous faites glisser ou redimensionnez la zone des webcams)",
"app.debugWindow.form.chatLoggerLabel": "Tester les niveaux de logs de la discussion.",
"app.debugWindow.form.button.apply": "Appliquer",
"app.layout.modal.title": "Mise en page",
"app.layout.modal.confirm": "Confirmer",
"app.layout.modal.cancel": "Annuler",
"app.layout.modal.layoutLabel": "Choisir votre mise en page",
"app.layout.modal.keepPushingLayoutLabel": "Appliquer la mise en page à tous",
"app.layout.modal.pushLayoutLabel": "Appliquer la mise en page à tout le monde",
"app.layout.modal.layoutToastLabel": "Le paramétrage de la mise en page a changé",
"app.layout.modal.layoutSingular": "Mise en page",
"app.layout.modal.layoutBtnDesc": "Défini la mise en page en tant qu'option sélectionnée",
"app.layout.style.custom": "Personnalisé",
"app.layout.style.smart": "Mise en page intelligente",
"app.layout.style.presentationFocus": "Centrer sur la présentation",
"app.layout.style.videoFocus": "Centrer sur la vidéo",
"app.layout.style.customPush": "Personnalisé (applique la disposition à tous les utilisateurs)",
"app.layout.style.smartPush": "Disposition automatique (applique la disposition à tous les utilisateurs)",
"app.layout.style.presentationFocusPush": "Centre sur la présentation (applique la disposition à tous les utilisateurs)",
"app.layout.style.videoFocusPush": "Centrer sur la vidéo (applique la disposition à tous les utilisateurs)",
"app.layout.style.customPush": "Personnalisé (applique la mise en page à tous les utilisateurs)",
"app.layout.style.smartPush": "Mise en page automatique (applique la mise en page à tous les utilisateurs)",
"app.layout.style.presentationFocusPush": "Centre sur la présentation (applique la mise en page à tous les utilisateurs)",
"app.layout.style.videoFocusPush": "Centrer sur la vidéo (applique la mise en page à tous les utilisateurs)",
"playback.button.about.aria": "À propos",
"playback.button.clear.aria": "Effacer la recherche",
"playback.button.close.aria": "Fermer la fenêtre de dialogue",
@ -995,6 +1230,7 @@
"playback.player.thumbnails.wrapper.aria": "Zone des vignettes",
"playback.player.webcams.wrapper.aria": "Zone des caméras",
"app.learningDashboard.dashboardTitle": "Tableau de bord d'activité des participants",
"app.learningDashboard.bigbluebuttonTitle": "BigBlueButton",
"app.learningDashboard.downloadSessionDataLabel": "Télécharger les données de la session",
"app.learningDashboard.lastUpdatedLabel": "Dernière mise à jour à",
"app.learningDashboard.sessionDataDownloadedLabel": "Téléchargé !",
@ -1019,6 +1255,12 @@
"app.learningDashboard.userDetails.response": "Réponse",
"app.learningDashboard.userDetails.mostCommonAnswer": "Réponse la plus fréquente",
"app.learningDashboard.userDetails.anonymousAnswer": "Sondage anonyme",
"app.learningDashboard.userDetails.talkTime": "Temps de parole",
"app.learningDashboard.userDetails.messages": "Messages",
"app.learningDashboard.userDetails.emojis": "Émoticônes",
"app.learningDashboard.userDetails.raiseHands": "Lever la main",
"app.learningDashboard.userDetails.pollVotes": "Sondages",
"app.learningDashboard.userDetails.onlineIndicator": "{0} temps en ligne",
"app.learningDashboard.usersTable.title": "Vue d'ensemble",
"app.learningDashboard.usersTable.colOnline": "Temps en ligne",
"app.learningDashboard.usersTable.colTalk": "Temps de conversation",
@ -1042,8 +1284,13 @@
"app.learningDashboard.pollsTable.anonymousRowName": "Anonyme",
"app.learningDashboard.pollsTable.noPollsCreatedHeading": "Aucun sondage n'a été créé",
"app.learningDashboard.pollsTable.noPollsCreatedMessage": "Une fois qu'un sondage a été envoyé aux utilisateurs, leurs résultats apparaissent dans cette liste.",
"app.learningDashboard.pollsTable.answerTotal": "Total",
"app.learningDashboard.pollsTable.userLabel": "Utilisateur",
"app.learningDashboard.statusTimelineTable.title": "Chronologie",
"app.learningDashboard.statusTimelineTable.thumbnail": "Vignette de présentation",
"app.learningDashboard.statusTimelineTable.presentation": "Présentation",
"app.learningDashboard.statusTimelineTable.pageNumber": "Page",
"app.learningDashboard.statusTimelineTable.setAt": "Régler à",
"app.learningDashboard.errors.invalidToken": "Jeton de session invalide",
"app.learningDashboard.errors.dataUnavailable": "Les données ne sont plus disponibles",
"mobileApp.portals.list.empty.addFirstPortal.label": "Ajoutez votre premier portail en utilisant le bouton ci-dessus,",
@ -1057,6 +1304,4 @@
"mobileApp.portals.addPortalPopup.validation.emptyFields": "Champs requis",
"mobileApp.portals.addPortalPopup.validation.portalNameAlreadyExists": "Ce nom est déjà utilisé",
"mobileApp.portals.addPortalPopup.validation.urlInvalid": "Erreur de chargement de la page - vérifiez l'URL et la connexion réseau"
}

View File

@ -246,6 +246,8 @@
"app.presentationUploader.sent": "Enviado",
"app.presentationUploader.exportingTimeout": "A exportación está tardando demasiado...",
"app.presentationUploader.export": "Enviar para o chat",
"app.presentationUploader.export.linkAvailable": "Ligazón para descargar {0} dispoñible no chat público.",
"app.presentationUploader.export.notAccessibleWarning": "pode non ser accesible",
"app.presentationUploader.currentPresentationLabel": "Presentación actual",
"app.presentationUploder.extraHint": "IMPORTANTE: cada ficheiro non pode exceder {0} MB e {1} páxinas.",
"app.presentationUploder.uploadLabel": "Enviar",
@ -264,6 +266,7 @@
"app.presentationUploder.rejectedError": "O(s) ficheiro(s) seleccionado(s) foi(foron) rexeitado(s). Revise o(os) tipo(s) de ficheiro.",
"app.presentationUploder.connectionClosedError": "Interrompeuse por mor dunha mala conectividade. Tenteo de novo.",
"app.presentationUploder.upload.progress": "Enviando ({0}%)",
"app.presentationUploder.conversion.204": "Non hai contido que capturar",
"app.presentationUploder.upload.413": "O ficheiro é demasiado grande, superou o máximo de {0} MB",
"app.presentationUploder.genericError": "Ouh! algo foi mal…",
"app.presentationUploder.upload.408": "Solicitar o tempo de espera do testemuño de envío.",
@ -454,6 +457,68 @@
"app.submenu.application.paginationEnabledLabel": "Paxinación do vídeo",
"app.submenu.application.layoutOptionLabel": "Tipo de disposición",
"app.submenu.application.pushLayoutLabel": "Forzar deseño",
"app.submenu.application.localeDropdown.af": "Afrikaans",
"app.submenu.application.localeDropdown.ar": "Árabe",
"app.submenu.application.localeDropdown.az": "Azerbaiyano",
"app.submenu.application.localeDropdown.bg-BG": "Búlgaro",
"app.submenu.application.localeDropdown.bn": "Bengalí",
"app.submenu.application.localeDropdown.ca": "Catalán",
"app.submenu.application.localeDropdown.cs-CZ": "Checo",
"app.submenu.application.localeDropdown.da": "Danés",
"app.submenu.application.localeDropdown.de": "Alemán",
"app.submenu.application.localeDropdown.dv": "Divehi",
"app.submenu.application.localeDropdown.el-GR": "Grego (Grecia)",
"app.submenu.application.localeDropdown.en": "Inglés",
"app.submenu.application.localeDropdown.eo": "Esperanto",
"app.submenu.application.localeDropdown.es": "Español",
"app.submenu.application.localeDropdown.es-419": "Español (América Latina)",
"app.submenu.application.localeDropdown.es-ES": "Español (España)",
"app.submenu.application.localeDropdown.es-MX": "Español (México)",
"app.submenu.application.localeDropdown.et": "Estonio",
"app.submenu.application.localeDropdown.eu": "Vasco",
"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": "Galego",
"app.submenu.application.localeDropdown.he": "Hebreo",
"app.submenu.application.localeDropdown.hi-IN": "Hindi",
"app.submenu.application.localeDropdown.hr": "Croata",
"app.submenu.application.localeDropdown.hu-HU": "Húngaro",
"app.submenu.application.localeDropdown.hy": "Armenio",
"app.submenu.application.localeDropdown.id": "Indonesio",
"app.submenu.application.localeDropdown.it-IT": "Italiano",
"app.submenu.application.localeDropdown.ja": "Xaponés",
"app.submenu.application.localeDropdown.ka": "Xeorxiano",
"app.submenu.application.localeDropdown.km": "Khmer",
"app.submenu.application.localeDropdown.kn": "Kannada",
"app.submenu.application.localeDropdown.ko-KR": "Coreano (Corea)",
"app.submenu.application.localeDropdown.lo-LA": "Laosiano",
"app.submenu.application.localeDropdown.lt-LT": "Lituano",
"app.submenu.application.localeDropdown.lv": "Letón",
"app.submenu.application.localeDropdown.ml": "Malayalam",
"app.submenu.application.localeDropdown.mn-MN": "Mongol",
"app.submenu.application.localeDropdown.nb-NO": "Noruegués (bokmal)",
"app.submenu.application.localeDropdown.nl": "Holandés",
"app.submenu.application.localeDropdown.oc": "Occitano",
"app.submenu.application.localeDropdown.pl-PL": "Polaco",
"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": "Ruso",
"app.submenu.application.localeDropdown.sk-SK": "Eslovaco (Eslovaquia)",
"app.submenu.application.localeDropdown.sl": "Esloveno",
"app.submenu.application.localeDropdown.sr": "Serbio",
"app.submenu.application.localeDropdown.sv-SE": "Sueco",
"app.submenu.application.localeDropdown.ta": "Támil",
"app.submenu.application.localeDropdown.te": "Telugú",
"app.submenu.application.localeDropdown.th": "Tailandés",
"app.submenu.application.localeDropdown.tr": "Turco",
"app.submenu.application.localeDropdown.tr-TR": "Turco (Turquía)",
"app.submenu.application.localeDropdown.uk-UA": "Ucraíno",
"app.submenu.application.localeDropdown.vi": "Vietnamita",
"app.submenu.application.localeDropdown.vi-VN": "Vietnamita (Vietnam)",
"app.submenu.application.localeDropdown.zh-CN": "Chinés simplificado (China)",
"app.submenu.application.localeDropdown.zh-TW": "Chinés tradicional (Taiwán)",
"app.submenu.notification.SectionTitle": "Notificacións",
"app.submenu.notification.Desc": "Definir como e que se lle notificará.",
"app.submenu.notification.audioAlertLabel": "Avisos sonoros",

View File

@ -246,6 +246,8 @@
"app.presentationUploader.sent": "Ուղարկված է",
"app.presentationUploader.exportingTimeout": "Արտահանումը տևում շատ երկար․․․",
"app.presentationUploader.export": "Ուղարկել զրուցարան",
"app.presentationUploader.export.linkAvailable": " {0}-ի բեռնելու հղումը հասանելի է ընդհանուր զրուցարանում։",
"app.presentationUploader.export.notAccessibleWarning": "կարող է թույլատրված չլինել",
"app.presentationUploader.currentPresentationLabel": "Ընթացիկ ներկայացումը",
"app.presentationUploder.extraHint": "ԿԱՐԵՎՈՐ - յուրաքանչյուր ֆայլը չպետք է գերազանցի ֆայլը {0} մեգաբայթը և {1} էջերի քանակը։",
"app.presentationUploder.uploadLabel": "Բեռնել",
@ -454,6 +456,68 @@
"app.submenu.application.paginationEnabledLabel": "Տեսանյութի մասնատում",
"app.submenu.application.layoutOptionLabel": "Դասավորվածության տեսակը",
"app.submenu.application.pushLayoutLabel": "Push դասավորվածություն",
"app.submenu.application.localeDropdown.af": "Աֆրիկերեն",
"app.submenu.application.localeDropdown.ar": "Արաբերեն",
"app.submenu.application.localeDropdown.az": "Նոր հորինված լեզու",
"app.submenu.application.localeDropdown.bg-BG": "Բուլղարերեն",
"app.submenu.application.localeDropdown.bn": "Բենգալի",
"app.submenu.application.localeDropdown.ca": "Կատալոներեն",
"app.submenu.application.localeDropdown.cs-CZ": "Չեխերեն",
"app.submenu.application.localeDropdown.da": "Դանիերեն",
"app.submenu.application.localeDropdown.de": "Գերմաներեն",
"app.submenu.application.localeDropdown.dv": "Դիվեհի",
"app.submenu.application.localeDropdown.el-GR": "Հունարեն",
"app.submenu.application.localeDropdown.en": "Անգլերեն",
"app.submenu.application.localeDropdown.eo": "Էսպերանտո",
"app.submenu.application.localeDropdown.es": "Իսպաներեն",
"app.submenu.application.localeDropdown.es-419": "Իսպաներեն (Լատինամերիկյան)",
"app.submenu.application.localeDropdown.es-ES": "Իսպաներեն (Իսպանիա)",
"app.submenu.application.localeDropdown.es-MX": "Իսպաներեն (Մեքսիկա)",
"app.submenu.application.localeDropdown.et": "Էստոներեն",
"app.submenu.application.localeDropdown.eu": "Բասկերեն",
"app.submenu.application.localeDropdown.fa-IR": "Պարսկերեն",
"app.submenu.application.localeDropdown.fi": "Ֆիներեն",
"app.submenu.application.localeDropdown.fr": "Ֆրանսերեն",
"app.submenu.application.localeDropdown.gl": "Գալիչի",
"app.submenu.application.localeDropdown.he": "Եբրայերեն",
"app.submenu.application.localeDropdown.hi-IN": "Հնդկերեն",
"app.submenu.application.localeDropdown.hr": "Խորվաթերեն",
"app.submenu.application.localeDropdown.hu-HU": "Հունագարերեն",
"app.submenu.application.localeDropdown.hy": "Հայերեն",
"app.submenu.application.localeDropdown.id": "Ինդոնեզերեն",
"app.submenu.application.localeDropdown.it-IT": "Իտալերեն",
"app.submenu.application.localeDropdown.ja": "Ճապոներեն",
"app.submenu.application.localeDropdown.ka": "Վրացերեն",
"app.submenu.application.localeDropdown.km": "Խմերի",
"app.submenu.application.localeDropdown.kn": "Կաննադա",
"app.submenu.application.localeDropdown.ko-KR": "Կորերեն (Կորեա)",
"app.submenu.application.localeDropdown.lo-LA": "Լաո",
"app.submenu.application.localeDropdown.lt-LT": "Լիտվերեն",
"app.submenu.application.localeDropdown.lv": "Լատվերեն",
"app.submenu.application.localeDropdown.ml": "Մալայալամ",
"app.submenu.application.localeDropdown.mn-MN": "Մոնղոլերեն",
"app.submenu.application.localeDropdown.nb-NO": "Նորվեգերեն (բոկմալ)",
"app.submenu.application.localeDropdown.nl": "Հոլանդերեն",
"app.submenu.application.localeDropdown.oc": "Օքսիտաներեն",
"app.submenu.application.localeDropdown.pl-PL": "Լեհերեն",
"app.submenu.application.localeDropdown.pt": "Պորտուգալերեն",
"app.submenu.application.localeDropdown.pt-BR": "Պորտուգալերեն (Բրազիլիա)",
"app.submenu.application.localeDropdown.ro-RO": "Ռումիներեն",
"app.submenu.application.localeDropdown.ru": "Ռուսերեն",
"app.submenu.application.localeDropdown.sk-SK": "Սլովակերեն (Սլովակիա)",
"app.submenu.application.localeDropdown.sl": "Սլովեներեն",
"app.submenu.application.localeDropdown.sr": "Սերբերեն",
"app.submenu.application.localeDropdown.sv-SE": "Շվեդերեն",
"app.submenu.application.localeDropdown.ta": "Թամիլերեն",
"app.submenu.application.localeDropdown.te": "Թելուգու",
"app.submenu.application.localeDropdown.th": "Թայլաներեն",
"app.submenu.application.localeDropdown.tr": "Թուրքերեն",
"app.submenu.application.localeDropdown.tr-TR": "Թուրքերեն (Թուրքիա)",
"app.submenu.application.localeDropdown.uk-UA": "Ուկրաիներեն",
"app.submenu.application.localeDropdown.vi": "Վիետնամերեն",
"app.submenu.application.localeDropdown.vi-VN": "Վիետնամերեն (Վիետնամ)",
"app.submenu.application.localeDropdown.zh-CN": "Չինարեն պարզեցված (Չինաստան)",
"app.submenu.application.localeDropdown.zh-TW": "Ավանդական չինարեն (Թայվան)",
"app.submenu.notification.SectionTitle": "Ծանուցումներ",
"app.submenu.notification.Desc": "Սահմանել, թե երբ և ինչի մասին ծանուցվեք",
"app.submenu.notification.audioAlertLabel": "Ձայնային ծանուցումներ",

View File

@ -3,8 +3,8 @@
"app.chat.submitLabel": "メッセージを送信",
"app.chat.loading": "チャットメッセージをロードしています: {0}%",
"app.chat.errorMaxMessageLength": "メッセージが長すぎます。最大の{0}文字を超えています",
"app.chat.disconnected": "通信が切断されたため、メッセージを送ません",
"app.chat.locked": "チャットがロック状態のため、メッセージを送ません",
"app.chat.disconnected": "通信が切断されたため、メッセージを送ることができません",
"app.chat.locked": "チャットがロック状態のため、メッセージを送ることができません",
"app.chat.inputLabel": "チャット {0} へメッセージ入力",
"app.chat.emojiButtonLabel": "絵文字選択",
"app.chat.inputPlaceholder": "メッセージ{0}",
@ -28,8 +28,8 @@
"app.chat.multi.typing": "複数の人が入力中",
"app.chat.one.typing": "{0}が入力中",
"app.chat.two.typing": "{0}と{1}が入力中",
"app.chat.copySuccess": "チャットの記録がコピーされました",
"app.chat.copyErr": "チャット記録のコピーに失敗しました",
"app.chat.copySuccess": "チャットのやりとりがコピーされました",
"app.chat.copyErr": "チャットのやりとりのコピーに失敗しました",
"app.emojiPicker.search": "検索",
"app.emojiPicker.notFound": "絵文字が見つかりません",
"app.emojiPicker.skintext": "標準の肌の色を選択",
@ -246,6 +246,8 @@
"app.presentationUploader.sent": "送信終了",
"app.presentationUploader.exportingTimeout": "エクスポートに時間がかかりすぎています...",
"app.presentationUploader.export": "チャットへ送信",
"app.presentationUploader.export.linkAvailable": "{0}をダウンロードするためのリンクが、公開チャットから利用できます。",
"app.presentationUploader.export.notAccessibleWarning": "にはアクセスできないようです",
"app.presentationUploader.currentPresentationLabel": "プレゼンテーションファイル",
"app.presentationUploder.extraHint": "重要:それぞれのファイルが{0}MB、{1}ページを超えないようにしてください。",
"app.presentationUploder.uploadLabel": "アップロード",
@ -264,6 +266,7 @@
"app.presentationUploder.rejectedError": "選択されたファイルはアップロードできません。ファイル形式を確認してください。",
"app.presentationUploder.connectionClosedError": "接続に問題があったため中断されました。もう一度試してみてください。",
"app.presentationUploder.upload.progress": "アップロード中({0}%)",
"app.presentationUploder.conversion.204": "内容が読み取れませんでした",
"app.presentationUploder.upload.413": "ファイルが大きすぎます。最大サイズは {0} MBです。",
"app.presentationUploder.genericError": "申し訳ありませんが、何か問題があるようです。",
"app.presentationUploder.upload.408": "アップロード許可の要求が時間切れになりました。",
@ -409,7 +412,7 @@
"app.endMeeting.description": "この操作により、会議を終了します。まだ{0}人が参加していますが本当に構いませんか?",
"app.endMeeting.noUserDescription": "本当にこの会議を終了しますか?",
"app.endMeeting.contentWarning": "この会議のチャットメッセージ、共有メモ、ホワイトボードの書き込み、そして共有ドキュメントにはもうアクセスできなくなります",
"app.endMeeting.yesLabel": "ユーザー全員を退室させる",
"app.endMeeting.yesLabel": "はい、全員退室させて終了します",
"app.endMeeting.noLabel": "いいえ",
"app.about.title": "このソフトウェアについて",
"app.about.version": "ビルド番号:",
@ -454,6 +457,68 @@
"app.submenu.application.paginationEnabledLabel": "ビデオのページ付け",
"app.submenu.application.layoutOptionLabel": "レイアウトのタイプ",
"app.submenu.application.pushLayoutLabel": "レイアウトを強制",
"app.submenu.application.localeDropdown.af": "アフリカーンス語",
"app.submenu.application.localeDropdown.ar": "アラビア語",
"app.submenu.application.localeDropdown.az": "アゼルバイジャン語",
"app.submenu.application.localeDropdown.bg-BG": "ブルガリア語",
"app.submenu.application.localeDropdown.bn": "ベンガル語",
"app.submenu.application.localeDropdown.ca": "カタルーニャ語",
"app.submenu.application.localeDropdown.cs-CZ": "チェコ語",
"app.submenu.application.localeDropdown.da": "デンマーク語",
"app.submenu.application.localeDropdown.de": "ドイツ語",
"app.submenu.application.localeDropdown.dv": "ディベヒ語",
"app.submenu.application.localeDropdown.el-GR": "ギリシャ語(ギリシャ)",
"app.submenu.application.localeDropdown.en": "英語",
"app.submenu.application.localeDropdown.eo": "エスペラント語",
"app.submenu.application.localeDropdown.es": "スペイン語",
"app.submenu.application.localeDropdown.es-419": "スペイン語(南米)",
"app.submenu.application.localeDropdown.es-ES": "スペイン語(スペイン)",
"app.submenu.application.localeDropdown.es-MX": "スペイン語(メキシコ)",
"app.submenu.application.localeDropdown.et": "エストニア語",
"app.submenu.application.localeDropdown.eu": "バスク語",
"app.submenu.application.localeDropdown.fa-IR": "ペルシャ語",
"app.submenu.application.localeDropdown.fi": "フィンランド語",
"app.submenu.application.localeDropdown.fr": "フランス語",
"app.submenu.application.localeDropdown.gl": "ガリシア語",
"app.submenu.application.localeDropdown.he": "ヘブライ語",
"app.submenu.application.localeDropdown.hi-IN": "ヒンズー語",
"app.submenu.application.localeDropdown.hr": "クロアチア語",
"app.submenu.application.localeDropdown.hu-HU": "ハンガリア語",
"app.submenu.application.localeDropdown.hy": "アルメニア語",
"app.submenu.application.localeDropdown.id": "インドネシア語",
"app.submenu.application.localeDropdown.it-IT": "イタリア語",
"app.submenu.application.localeDropdown.ja": "日本語",
"app.submenu.application.localeDropdown.ka": "ジョージア語",
"app.submenu.application.localeDropdown.km": "クメール語",
"app.submenu.application.localeDropdown.kn": "カンナダ語",
"app.submenu.application.localeDropdown.ko-KR": "韓国語(韓国)",
"app.submenu.application.localeDropdown.lo-LA": "ラオス語",
"app.submenu.application.localeDropdown.lt-LT": "リトアニア語",
"app.submenu.application.localeDropdown.lv": "ラトビア語",
"app.submenu.application.localeDropdown.ml": "マラヤーラム語",
"app.submenu.application.localeDropdown.mn-MN": "モンゴル語",
"app.submenu.application.localeDropdown.nb-NO": "ノルウェー語(ブークモール)",
"app.submenu.application.localeDropdown.nl": "オランダ語",
"app.submenu.application.localeDropdown.oc": "オック語",
"app.submenu.application.localeDropdown.pl-PL": "ポーランド語",
"app.submenu.application.localeDropdown.pt": "ポルトガル語",
"app.submenu.application.localeDropdown.pt-BR": "ポルトガル語(ブラジル)",
"app.submenu.application.localeDropdown.ro-RO": "ルーマニア語",
"app.submenu.application.localeDropdown.ru": "ロシア語",
"app.submenu.application.localeDropdown.sk-SK": "スロバキア語(スロバキア)",
"app.submenu.application.localeDropdown.sl": "スロベニア語",
"app.submenu.application.localeDropdown.sr": "セルビア語",
"app.submenu.application.localeDropdown.sv-SE": "スウェーデン語",
"app.submenu.application.localeDropdown.ta": "タミル語",
"app.submenu.application.localeDropdown.te": "テルグ語",
"app.submenu.application.localeDropdown.th": "タイ語",
"app.submenu.application.localeDropdown.tr": "トルコ語",
"app.submenu.application.localeDropdown.tr-TR": "トルコ語(トルコ)",
"app.submenu.application.localeDropdown.uk-UA": "ウクライナ語",
"app.submenu.application.localeDropdown.vi": "ベトナム語",
"app.submenu.application.localeDropdown.vi-VN": "ベトナム語(ベトナム)",
"app.submenu.application.localeDropdown.zh-CN": "中国語(中国)",
"app.submenu.application.localeDropdown.zh-TW": "中国語(台湾)",
"app.submenu.notification.SectionTitle": "通知",
"app.submenu.notification.Desc": "何をどのように通知するかを設定できます。",
"app.submenu.notification.audioAlertLabel": "音声通知",
@ -1162,7 +1227,7 @@
"playback.player.presentation.wrapper.aria": "プレゼンエリア",
"playback.player.screenshare.wrapper.aria": "画面共有エリア",
"playback.player.search.modal.title": "検索",
"playback.player.search.modal.subtitle": "プレゼンスライドの内容を探す",
"playback.player.search.modal.subtitle": "スライドのテキストを検索",
"playback.player.thumbnails.wrapper.aria": "サムネイルエリア",
"playback.player.webcams.wrapper.aria": "ウェブカムエリア",
"app.learningDashboard.dashboardTitle": "参加状況分析ボード",

View File

@ -1,5 +1,5 @@
{
"app.home.greeting": "Ваша презентация скоро начнется...",
"app.home.greeting": "Вебинар скоро начнется...",
"app.chat.submitLabel": "Отправить сообщение",
"app.chat.loading": "Загружено сообщений чата: {0}%",
"app.chat.errorMaxMessageLength": "Сообщение слишком длинное, максимум {0} символов",
@ -119,12 +119,12 @@
"app.userList.menu.removeUser.label": "Исключить пользователя",
"app.userList.menu.removeConfirmation.label": "Исключить пользователя ({0})",
"app.userlist.menu.removeConfirmation.desc": "Запретить пользователю повторно подключаться к вебинару",
"app.userList.menu.muteUserAudio.label": "Выключить микрофон пользователя",
"app.userList.menu.unmuteUserAudio.label": "Включить микрофон пользователя",
"app.userList.menu.muteUserAudio.label": "Выключить микрофон",
"app.userList.menu.unmuteUserAudio.label": "Включить микрофон",
"app.userList.menu.webcamPin.label": "Закрепить веб-камеру",
"app.userList.menu.webcamUnpin.label": "Открепить веб-камеру",
"app.userList.menu.giveWhiteboardAccess.label" : "Дать право рисования на доске этому пользователю",
"app.userList.menu.removeWhiteboardAccess.label": "Забрать право рисования на доске у этого пользователя",
"app.userList.menu.giveWhiteboardAccess.label" : "Дать право рисования на доске",
"app.userList.menu.removeWhiteboardAccess.label": "Забрать право рисования на доске",
"app.userList.menu.ejectUserCameras.label": "Закрыть камеры",
"app.userList.userAriaLabel": "{0} {1} {2} Статус {3}",
"app.userList.menu.promoteUser.label": "Повысить до модератора",
@ -134,12 +134,12 @@
"app.userList.menu.directoryLookup.label": "Поиск в каталоге",
"app.userList.menu.makePresenter.label": "Сделать ведущим",
"app.userList.userOptions.manageUsersLabel": "Управление пользователями",
"app.userList.userOptions.muteAllLabel": "Выключить микрофон всем пользователям",
"app.userList.userOptions.muteAllDesc": "Выключить микрофоны у всех пользователей",
"app.userList.userOptions.muteAllLabel": "Выключить микрофон всем",
"app.userList.userOptions.muteAllDesc": "Выключить микрофоны у всех",
"app.userList.userOptions.clearAllLabel": "Убрать все иконки статуса",
"app.userList.userOptions.clearAllDesc": "Убрать все иконки статуса у пользователей",
"app.userList.userOptions.muteAllExceptPresenterLabel": "Заблокировать микрофоны всем кроме ведущего",
"app.userList.userOptions.muteAllExceptPresenterDesc": "Заблокировать микрофоны у всех пользователей, кроме ведущего",
"app.userList.userOptions.muteAllExceptPresenterLabel": "Выключить микрофоны всем, кроме ведущего",
"app.userList.userOptions.muteAllExceptPresenterDesc": "Выключить микрофоны всем, кроме ведущего",
"app.userList.userOptions.unmuteAllLabel": "Отменить блокировку микрофонов",
"app.userList.userOptions.unmuteAllDesc": "Снять блокировку микрофонов",
"app.userList.userOptions.lockViewersLabel": "Заблокировать пользователей",
@ -169,7 +169,7 @@
"app.media.label": "Медиа",
"app.media.autoplayAlertDesc": "Разрешить доступ",
"app.media.screenshare.start": "Демонстрация экрана началась",
"app.media.screenshare.end": "Демонстрация экрана закончилась",
"app.media.screenshare.end": "Демонстрация экрана завершилась",
"app.media.screenshare.endDueToDataSaving": "Демонстрация экрана завершена по причине сохранения данных",
"app.media.screenshare.unavailable": "Демонстрация экрана недоступна",
"app.media.screenshare.notSupported": "Демонстрация экрана не поддерживается браузером",
@ -183,18 +183,18 @@
"app.screenshare.screenshareRetryOtherEnvError": "Код {0}. Невозможно предоставить общий доступ к экрану. Попробуйте ещё раз, используя другой браузер или устройство.",
"app.screenshare.screenshareUnsupportedEnv": "Код {0}. Браузер не поддерживается. Попробуйте ещё раз, используя другой браузер или устройство.",
"app.screenshare.screensharePermissionError": "Код {0}. Разрешение на доступ к экрану не было предоставлено.",
"app.meeting.ended": "Вебинар окончен",
"app.meeting.ended": "Вебинар завершён",
"app.meeting.meetingTimeRemaining": "До окончания вебинара осталось: {0}",
"app.meeting.meetingTimeHasEnded": "Время вышло. Вебинар скоро закроется.",
"app.meeting.meetingTimeHasEnded": "Время вышло. Вебинар скоро завершится.",
"app.meeting.endedByUserMessage": "Этот вебинар был завершен {0}",
"app.meeting.endedByNoModeratorMessageSingular": "Встреча закончилась из-за отсутствия модератора более одной минуты",
"app.meeting.endedByNoModeratorMessagePlural": "Встреча закончилась из-за отсутствия модератора более {0} минут",
"app.meeting.endedByNoModeratorMessageSingular": "Вебинар завершен из-за отсутствия модератора более одной минуты",
"app.meeting.endedByNoModeratorMessagePlural": "Вебинар завершен из-за отсутствия модератора более {0} минут",
"app.meeting.endedMessage": "Вы будете перенаправлены назад на главный экран",
"app.meeting.alertMeetingEndsUnderMinutesSingular": "Вебинар завершится в течение минуты.",
"app.meeting.alertMeetingEndsUnderMinutesPlural": "Вебинар завершится через {0} минут.",
"app.meeting.alertBreakoutEndsUnderMinutesPlural": "Перерыв заканчивается через {0} минут.",
"app.meeting.alertBreakoutEndsUnderMinutesSingular": "Перерыв закончится в течение минуты.",
"app.presentation.hide": "Скрыть презентацию",
"app.meeting.alertBreakoutEndsUnderMinutesPlural": "Групповая работа завершится через {0} минут.",
"app.meeting.alertBreakoutEndsUnderMinutesSingular": "Групповая работа завершится в течение минуты.",
"app.presentation.hide": "Свернуть презентацию",
"app.presentation.notificationLabel": "Текущая презентация",
"app.presentation.downloadLabel": "Скачать",
"app.presentation.slideContent": "Содержимое слайда",
@ -203,7 +203,7 @@
"app.presentation.changedSlideContent": "Слайд презентации изменен на {0}",
"app.presentation.emptySlideContent": "Текущий слайд не содержит никакой информации",
"app.presentation.options.fullscreen": "Полноэкранная презентация",
"app.presentation.options.exitFullscreen": "Выход из режима полного экрана",
"app.presentation.options.exitFullscreen": "Выход из полноэкранного режима",
"app.presentation.options.minimize": "Свернуть",
"app.presentation.options.snapshot": "Снимок текущего слайда",
"app.presentation.options.downloading": "Загрузка...",
@ -223,7 +223,7 @@
"app.presentation.presentationToolbar.fitScreenLabel": "Подогнать к экрану",
"app.presentation.presentationToolbar.fitScreenDesc": "Уместить слайд целиком",
"app.presentation.presentationToolbar.zoomLabel": "Масштаб",
"app.presentation.presentationToolbar.zoomDesc": "Изменить масштаб презетации",
"app.presentation.presentationToolbar.zoomDesc": "Изменить масштаб",
"app.presentation.presentationToolbar.zoomInLabel": "Увеличить",
"app.presentation.presentationToolbar.zoomInDesc": "Увеличить презентацию",
"app.presentation.presentationToolbar.zoomOutLabel": "Уменьшить",
@ -246,6 +246,8 @@
"app.presentationUploader.sent": "Отправить",
"app.presentationUploader.exportingTimeout": "Экспорт займёт много времени...",
"app.presentationUploader.export": "Отправить в чат",
"app.presentationUploader.export.linkAvailable": "Ссылка для скачивания {0} доступна в публичном чате.",
"app.presentationUploader.export.notAccessibleWarning": "может быть недоступен",
"app.presentationUploader.currentPresentationLabel": "Текущая презентация",
"app.presentationUploder.extraHint": "ВАЖНО: размер каждого файла не может превышать {0} МБ и {1} страниц.",
"app.presentationUploder.uploadLabel": "Загрузить",
@ -264,6 +266,7 @@
"app.presentationUploder.rejectedError": "Загрузка выбранных файлов была отменена. Пожалуйста проверьте тип файла (файлов)",
"app.presentationUploder.connectionClosedError": "Прервано из-за нестабильного соединения. Пожалуйста, попробуйте снова.",
"app.presentationUploder.upload.progress": "Загрузка ({0}%)",
"app.presentationUploder.conversion.204": "Нет контента для захвата",
"app.presentationUploder.upload.413": "Размер файла слишком большой. Максимум: {0} МБ",
"app.presentationUploder.genericError": "Упс, Что-то пошло не так...",
"app.presentationUploder.upload.408": "Токен запроса загрузки истёк.",
@ -305,7 +308,7 @@
"app.poll.quickPollTitle": "Быстрое голосование",
"app.poll.hidePollDesc": "Скрыть меню голосования",
"app.poll.quickPollInstruction": "Чтобы начать голосование, выберите вариант ниже.",
"app.poll.activePollInstruction": "Оставьте данное меню открытым, чтобы видеть ответы пользователей в реальном времени. Когда вы будете готовы, нажмите 'опубликовать' для публикации результатов и завершения голосования.",
"app.poll.activePollInstruction": "Оставьте данное меню открытым, чтобы видеть ответы пользователей в реальном времени. Нажмите 'опубликовать' для публикации результатов и завершения голосования.",
"app.poll.dragDropPollInstruction": "Чтобы вставить варианты ответа в голосование, перенесите текстовый документ с вариантами ответа для голосования в выделенное поле",
"app.poll.customPollTextArea": "Вставить варианты ответов для голосования",
"app.poll.publishLabel": "Опубликовать",
@ -336,7 +339,7 @@
"app.poll.deleteRespDesc": "Удалить вариант ответа {0}",
"app.poll.t": "Верно",
"app.poll.f": "Неверно",
"app.poll.tf": "Верно/Неверно",
"app.poll.tf": "Верно / Неверно",
"app.poll.y": "Да",
"app.poll.n": "Нет",
"app.poll.abstention": "Воздержусь",
@ -369,10 +372,10 @@
"app.polling.responseNotSecret": "Открытое голосование - Ведущий увидит Ваш ответ.",
"app.polling.pollAnswerLabel": "Результат голосования {0}",
"app.polling.pollAnswerDesc": "Выберите этот вариант, чтобы проголосовать за {0}",
"app.failedMessage": "Извините, проблемы с подключением к серверу.",
"app.failedMessage": "Проблемы с подключением к серверу.",
"app.downloadPresentationButton.label": "Скачать оригинальную презентацию",
"app.connectingMessage": "Подключение...",
"app.waitingMessage": "Соединение потеряно. Попытка переподключения через {0} секунд...",
"app.waitingMessage": "Соединение потеряно. Повторное подключение через {0} секунд...",
"app.retryNow": "Повторить",
"app.muteWarning.label": "Щёлкните {0} чтобы включить свой микрофон.",
"app.muteWarning.disableMessage": "Уведомления о выключенном микрофоне отключены до следующего включения микрофона",
@ -406,7 +409,7 @@
"app.leaveConfirmation.confirmLabel": "Выйти",
"app.leaveConfirmation.confirmDesc": "Выйти из вебинара",
"app.endMeeting.title": "Завершить {0}",
"app.endMeeting.description": "Вы уверены, что хотите завершить вебинар для {0} пользователей?",
"app.endMeeting.description": "Вы уверены, что хотите завершить вебинар для {0} пользователей?",
"app.endMeeting.noUserDescription": "Вы уверены, что хотите завершить этот вебинар?",
"app.endMeeting.contentWarning": "В этом вебинаре больше не доступны: Сообщения в чате, общие заметки, записи на доске и общие документы.",
"app.endMeeting.yesLabel": "Завершить вебинар для всех пользователей",
@ -434,8 +437,8 @@
"app.actionsBar.camOffLabel": "Выключить камеру",
"app.actionsBar.raiseLabel": "Поднять",
"app.actionsBar.label": "Панель действий",
"app.actionsBar.actionsDropdown.restorePresentationLabel": "Восстановить презентацию",
"app.actionsBar.actionsDropdown.restorePresentationDesc": "Кнопка для восстановления презентации после ее сворачивания",
"app.actionsBar.actionsDropdown.restorePresentationLabel": "Развернуть презентацию",
"app.actionsBar.actionsDropdown.restorePresentationDesc": "Кнопка для разворачивания презентации после ее сворачивания",
"app.actionsBar.actionsDropdown.minimizePresentationLabel": "Свернуть презентацию",
"app.actionsBar.actionsDropdown.minimizePresentationDesc": "Кнопка для сворачивания презентации",
"app.actionsBar.actionsDropdown.layoutModal": "Настройки компоновки",
@ -454,10 +457,72 @@
"app.submenu.application.paginationEnabledLabel": "Разделение видео",
"app.submenu.application.layoutOptionLabel": "Тип компоновки",
"app.submenu.application.pushLayoutLabel": "Применить компоновку",
"app.submenu.application.localeDropdown.af": "Африканский",
"app.submenu.application.localeDropdown.ar": "Арабский",
"app.submenu.application.localeDropdown.az": "Азербайджанский",
"app.submenu.application.localeDropdown.bg-BG": "Болгарский",
"app.submenu.application.localeDropdown.bn": "Бенгальский",
"app.submenu.application.localeDropdown.ca": "Каталанский",
"app.submenu.application.localeDropdown.cs-CZ": "Чешский",
"app.submenu.application.localeDropdown.da": "Датский",
"app.submenu.application.localeDropdown.de": "Немецкий",
"app.submenu.application.localeDropdown.dv": "Дивехи",
"app.submenu.application.localeDropdown.el-GR": "Греческий",
"app.submenu.application.localeDropdown.en": "Английский",
"app.submenu.application.localeDropdown.eo": "Эсперанто",
"app.submenu.application.localeDropdown.es": "Испанский",
"app.submenu.application.localeDropdown.es-419": "Испанский (Лат.Америка)",
"app.submenu.application.localeDropdown.es-ES": "Испанский (Испания)",
"app.submenu.application.localeDropdown.es-MX": "Испания (Мехико)",
"app.submenu.application.localeDropdown.et": "Эстонский",
"app.submenu.application.localeDropdown.eu": "Баскский",
"app.submenu.application.localeDropdown.fa-IR": "Персидский",
"app.submenu.application.localeDropdown.fi": "Финский",
"app.submenu.application.localeDropdown.fr": "Французский",
"app.submenu.application.localeDropdown.gl": "Галисийский",
"app.submenu.application.localeDropdown.he": "Иврит",
"app.submenu.application.localeDropdown.hi-IN": "Индийский",
"app.submenu.application.localeDropdown.hr": "Хорватский",
"app.submenu.application.localeDropdown.hu-HU": "Венгерский",
"app.submenu.application.localeDropdown.hy": "Армянский",
"app.submenu.application.localeDropdown.id": "Индонезийский",
"app.submenu.application.localeDropdown.it-IT": "Итальянский",
"app.submenu.application.localeDropdown.ja": "Японский",
"app.submenu.application.localeDropdown.ka": "Грузинский",
"app.submenu.application.localeDropdown.km": "Кхмерский",
"app.submenu.application.localeDropdown.kn": "Каннада",
"app.submenu.application.localeDropdown.ko-KR": "Корейский (Корея)",
"app.submenu.application.localeDropdown.lo-LA": "Лаосский",
"app.submenu.application.localeDropdown.lt-LT": "Литовский",
"app.submenu.application.localeDropdown.lv": "Латышский",
"app.submenu.application.localeDropdown.ml": "Малаялам",
"app.submenu.application.localeDropdown.mn-MN": "Монгольский",
"app.submenu.application.localeDropdown.nb-NO": "Норвежский (Бокмаль)",
"app.submenu.application.localeDropdown.nl": "Голландский",
"app.submenu.application.localeDropdown.oc": "Окситанский",
"app.submenu.application.localeDropdown.pl-PL": "Польский",
"app.submenu.application.localeDropdown.pt": "Португальский",
"app.submenu.application.localeDropdown.pt-BR": "Португальский (Бразилия)",
"app.submenu.application.localeDropdown.ro-RO": "Румынский",
"app.submenu.application.localeDropdown.ru": "Русский",
"app.submenu.application.localeDropdown.sk-SK": "Словацкий (Словакия)",
"app.submenu.application.localeDropdown.sl": "Словенский",
"app.submenu.application.localeDropdown.sr": "Сербский",
"app.submenu.application.localeDropdown.sv-SE": "Шведский",
"app.submenu.application.localeDropdown.ta": "Тамильский",
"app.submenu.application.localeDropdown.te": "Телугу",
"app.submenu.application.localeDropdown.th": "Тайский",
"app.submenu.application.localeDropdown.tr": "Турецкий",
"app.submenu.application.localeDropdown.tr-TR": "Турецкий (Турция)",
"app.submenu.application.localeDropdown.uk-UA": "Украинский",
"app.submenu.application.localeDropdown.vi": "Вьетнамский",
"app.submenu.application.localeDropdown.vi-VN": "Вьетнамский (Вьетнам)",
"app.submenu.application.localeDropdown.zh-CN": "Китайский упр. (Китай)",
"app.submenu.application.localeDropdown.zh-TW": "Китайский трад. (Китай)",
"app.submenu.notification.SectionTitle": "Уведомления",
"app.submenu.notification.Desc": "Определить, как и о чём вы будете оповещаться.",
"app.submenu.notification.audioAlertLabel": "Звуковые оповещения",
"app.submenu.notification.pushAlertLabel": "Всплывающие оповещения",
"app.submenu.notification.audioAlertLabel": "Звуковые уведомления",
"app.submenu.notification.pushAlertLabel": "Всплывающие уведомления",
"app.submenu.notification.messagesLabel": "Сообщение чата",
"app.submenu.notification.userJoinLabel": "Пользователь подключился",
"app.submenu.notification.userLeaveLabel": "Пользователь вышел",
@ -477,7 +542,7 @@
"app.settings.usersTab.label": "Пользователи",
"app.settings.main.label": "Настройки",
"app.settings.main.cancel.label": "Отмена",
"app.settings.main.cancel.label.description": "Сбрасывает изменения и закрывает меню настроек",
"app.settings.main.cancel.label.description": "Отменяет изменения и закрывает меню настроек",
"app.settings.main.save.label": "Сохранить",
"app.settings.main.save.label.description": "Сохраняет изменения и закрывает меню настроек",
"app.settings.dataSavingTab.label": "Сохранение данных",
@ -511,11 +576,11 @@
"app.actionsBar.actionsDropdown.pollBtnDesc": "Включить/Выключить панель голосования",
"app.actionsBar.actionsDropdown.saveUserNames": "Сохранить имена пользователей",
"app.actionsBar.actionsDropdown.createBreakoutRoom": "Создать комнаты для групповой работы",
"app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "Создать комнаты для групповой работы и распределить по ним пользователей",
"app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "Создать комнаты для групповой работы и распределить пользователей",
"app.actionsBar.actionsDropdown.captionsLabel": "Написать скрытые субтитры",
"app.actionsBar.actionsDropdown.captionsDesc": "Включает панель субтитров",
"app.actionsBar.actionsDropdown.takePresenter": "Стать ведущим",
"app.actionsBar.actionsDropdown.takePresenterDesc": "Назначить себя новым ведущим",
"app.actionsBar.actionsDropdown.takePresenterDesc": "Назначить себя ведущим",
"app.actionsBar.actionsDropdown.selectRandUserLabel": "Выбрать случайного пользователя",
"app.actionsBar.actionsDropdown.selectRandUserDesc": "Выбирает одного случайного пользователя из всех",
"app.actionsBar.actionsDropdown.propagateLayoutLabel": "Выбрать компоновку",
@ -560,8 +625,8 @@
"app.audioNotification.mediaFailedMessage": "Ошибка getUserMicMedia, разрешены только безопасные источники",
"app.audioNotification.deviceChangeFailed": "Не удалось изменить аудио устройство. Проверьте его доступность",
"app.audioNotification.closeLabel": "Закрыть",
"app.audioNotificaion.reconnectingAsListenOnly": "Микрофон заблокирован для пользователей, вы подключились слушателем",
"app.breakoutJoinConfirmation.title": "Подключиться к комнате групповой работы",
"app.audioNotificaion.reconnectingAsListenOnly": "Микрофоны пользователей заблокированы, вы подключились как слушатель",
"app.breakoutJoinConfirmation.title": "Подключиться к комнате для групповой работы",
"app.breakoutJoinConfirmation.message": "Вы хотите подключиться к ",
"app.breakoutJoinConfirmation.confirmDesc": "Подключает вас в комнату для групповой работы",
"app.breakoutJoinConfirmation.dismissLabel": "Отмена",
@ -658,7 +723,7 @@
"app.audio.captions.select.pt-BR": "Португальский",
"app.audio.captions.select.ru-RU": "Русский",
"app.audio.captions.select.zh-CN": "Китайский",
"app.error.removed": "Вас удалили из вебинара",
"app.error.removed": "Вас исключили из вебинара",
"app.error.meeting.ended": "Вы вышли из вебинара",
"app.meeting.logout.duplicateUserEjectReason": "Пользователь с таким же идентификатором пытается подключиться к вебинару",
"app.meeting.logout.permissionEjectReason": "Исключен из-за нарушения разрешений.",
@ -671,7 +736,7 @@
"app.modal.close": "Закрыть",
"app.modal.close.description": "Сбрасывает изменения и закрывает окно",
"app.modal.confirm": "Готово",
"app.modal.newTab": "( откроет новую вкладку )",
"app.modal.newTab": "(откроет новую вкладку)",
"app.modal.confirm.description": "Сохранить изменения и закрыть окно",
"app.modal.randomUser.noViewers.description": "Недостаточно пользователей для случайного выбора",
"app.modal.randomUser.selected.description": "Вы были выбраны случайным образом",
@ -684,12 +749,12 @@
"app.dropdown.list.item.activeLabel": "Активный",
"app.error.400": "Неверный запрос",
"app.error.401": "Не авторизирован",
"app.error.403": "Вас удалили из вебинара",
"app.error.403": "Вас исключили из вебинара",
"app.error.404": "Не найдено",
"app.error.408": "Аутентификация не пройдена",
"app.error.409": "Ошибка",
"app.error.410": "Мероприятие завершено",
"app.error.500": "Упс, что-то пошло не так",
"app.error.410": "Вебинар завершён",
"app.error.500": "Что-то пошло не так",
"app.error.503": "Вы отключены",
"app.error.disconnected.rejoin": "Перезагрузите страницу для повторного подключения.",
"app.error.userLoggedOut": "Пользователь имеет недопустимый токен вебинара из-за выхода из системы",
@ -720,11 +785,11 @@
"app.userList.guest.waitingUsers": "Ожидаем пользователей",
"app.userList.guest.waitingUsersTitle": "Управление пользователями",
"app.userList.guest.optionTitle": "Просмотр ожидающих пользователей",
"app.userList.guest.allowAllAuthenticated": "Разрешить всем аутентифицированным пользователям",
"app.userList.guest.allowAllAuthenticated": "Разрешить всем аутентифицированным",
"app.userList.guest.allowAllGuests": "Разрешить всем гостям",
"app.userList.guest.allowEveryone": "Разрешить всем",
"app.userList.guest.denyEveryone": "Отказать всем",
"app.userList.guest.pendingUsers": "{0} Ожидающие пользователи",
"app.userList.guest.pendingUsers": "{0} ожидающих пользователей",
"app.userList.guest.noPendingUsers": "Нет ожидающих пользователей...",
"app.userList.guest.pendingGuestUsers": "{0} гостей ожидает",
"app.userList.guest.pendingGuestAlert": "Подключился к вебинару и ждет вашего одобрения",
@ -745,13 +810,13 @@
"app.toast.chat.pollClick": "Результаты голосования опубликованы. Нажмите сюда для просмотра",
"app.toast.clearedEmoji.label": "Статус эмодзи очищен",
"app.toast.setEmoji.label": "Ваш статус эмодзи: {0}",
"app.toast.meetingMuteOn.label": "Всем пользователям были выключены микрофоны",
"app.toast.meetingMuteOn.label": "Всем пользователям выключены микрофоны",
"app.toast.meetingMuteOnViewers.label": "Всем пользователям выключены микрофоны",
"app.toast.meetingMuteOff.label": "Блокировка микрофонов отменена",
"app.toast.setEmoji.raiseHand": "Вы подняли свою руку",
"app.toast.setEmoji.lowerHand": "Ваша рука была опущена",
"app.toast.promotedLabel": "Вы были повышены до Модератора",
"app.toast.demotedLabel": "Вы были понижены до пользователя",
"app.toast.demotedLabel": "Вы были понижены до Пользователя",
"app.notification.recordingStart": "Запись вебинара начата",
"app.notification.recordingStop": "Запись вебинара приостановлена",
"app.notification.recordingPaused": "Запись вебинара приостановлена",
@ -882,7 +947,7 @@
"app.videoPreview.quality.low": "Низкое",
"app.videoPreview.quality.medium": "Среднее",
"app.videoPreview.quality.high": "Высокое",
"app.videoPreview.quality.hd": "Высокое разрешение",
"app.videoPreview.quality.hd": "HD",
"app.videoPreview.cancelLabel": "Отмена",
"app.videoPreview.closeLabel": "Закрыть",
"app.videoPreview.findingWebcamsLabel": "Поиск веб-камер",
@ -909,7 +974,7 @@
"app.video.visualEffects": "Визуальные эффекты",
"app.video.advancedVideo": "Расширенные настройки",
"app.video.iceCandidateError": "Ошибка добавления ICE кандидата",
"app.video.iceConnectionStateError": "Connection failure (ICE error 1107)",
"app.video.iceConnectionStateError": "Ошибка подключения (ICE error 1107)",
"app.video.permissionError": "Ошибка. Проверьте разрешение на доступ к веб-камере.",
"app.video.sharingError": "Ошибка при включении веб-камеры",
"app.video.abortError": "Возникла какая-то проблема, не позволяющая использовать устройство",
@ -1038,7 +1103,7 @@
"app.createBreakoutRoom.generatingURLMessage": "Мы создаем ссылку для подключения к выбранной комнате. Это может занять несколько секунд...",
"app.createBreakoutRoom.duration": "Продолжительность {0}",
"app.createBreakoutRoom.room": "Комната {0}",
"app.createBreakoutRoom.notAssigned": "Нераспределенные по комнатам пользователи ({0})",
"app.createBreakoutRoom.notAssigned": "Нераспределенные пользователи ({0})",
"app.createBreakoutRoom.join": "Подключиться к комнате",
"app.createBreakoutRoom.joinAudio": "Подключиться к аудио",
"app.createBreakoutRoom.returnAudio": "Вернуться в общий аудио",

View File

@ -246,6 +246,8 @@
"app.presentationUploader.sent": "Gönderildi",
"app.presentationUploader.exportingTimeout": "Dışa aktarma işlemi çok uzun sürüyor...",
"app.presentationUploader.export": "Sohbete gönder",
"app.presentationUploader.export.linkAvailable": "{0} indirmek için bağlantıyı herkese açık sohbette bulabilirsiniz.",
"app.presentationUploader.export.notAccessibleWarning": "erişilebilir olmayabilir",
"app.presentationUploader.currentPresentationLabel": "Geçerli sunum",
"app.presentationUploder.extraHint": "ÖNEMLİ: Her bir dosya {0} MB boyutundan ve {1} sayfadan küçük olmalıdır.",
"app.presentationUploder.uploadLabel": "Yükle",
@ -264,6 +266,7 @@
"app.presentationUploder.rejectedError": "Seçilmiş dosya(lar) reddedildi. Lütfen dosya türlerini denetleyin.",
"app.presentationUploder.connectionClosedError": "Kötü bağlantı nedeniyle kesintiye uğradı. Lütfen yeniden deneyin.",
"app.presentationUploder.upload.progress": "Yükleniyor ({0}%)",
"app.presentationUploder.conversion.204": "Yakalancak bir içerik bulunamadı",
"app.presentationUploder.upload.413": "Dosya boyutu çok büyük. En fazla {0} MB olabilir",
"app.presentationUploder.genericError": "Bir sorun çıktı...",
"app.presentationUploder.upload.408": "Yükleme isteği kodunun süresi geçmiş.",
@ -454,6 +457,68 @@
"app.submenu.application.paginationEnabledLabel": "Görüntü sayfalandırma",
"app.submenu.application.layoutOptionLabel": "Ekran düzeni türü",
"app.submenu.application.pushLayoutLabel": "Ekran düzenini gönder",
"app.submenu.application.localeDropdown.af": "Afrikaanca",
"app.submenu.application.localeDropdown.ar": "Arapça",
"app.submenu.application.localeDropdown.az": "Azerice",
"app.submenu.application.localeDropdown.bg-BG": "Bulgarca",
"app.submenu.application.localeDropdown.bn": "Bengalce",
"app.submenu.application.localeDropdown.ca": "Katalanca",
"app.submenu.application.localeDropdown.cs-CZ": "Çekce",
"app.submenu.application.localeDropdown.da": "Danca",
"app.submenu.application.localeDropdown.de": "Almanca",
"app.submenu.application.localeDropdown.dv": "Divehi",
"app.submenu.application.localeDropdown.el-GR": "Yunanca",
"app.submenu.application.localeDropdown.en": "İngilizce",
"app.submenu.application.localeDropdown.eo": "Esperanto",
"app.submenu.application.localeDropdown.es": "İspanyolca",
"app.submenu.application.localeDropdown.es-419": "İspanyolca (Latin Amerika)",
"app.submenu.application.localeDropdown.es-ES": "İspanyolca (İspanya)",
"app.submenu.application.localeDropdown.es-MX": "İspanyolca (Meksika)",
"app.submenu.application.localeDropdown.et": "Estonca",
"app.submenu.application.localeDropdown.eu": "Bask",
"app.submenu.application.localeDropdown.fa-IR": "Farsça",
"app.submenu.application.localeDropdown.fi": "Fince",
"app.submenu.application.localeDropdown.fr": "Fransızca",
"app.submenu.application.localeDropdown.gl": "Galiçce",
"app.submenu.application.localeDropdown.he": "İbranice",
"app.submenu.application.localeDropdown.hi-IN": "Hintçe",
"app.submenu.application.localeDropdown.hr": "Hırvatça",
"app.submenu.application.localeDropdown.hu-HU": "Macarca",
"app.submenu.application.localeDropdown.hy": "Ermenice",
"app.submenu.application.localeDropdown.id": "Endonezce",
"app.submenu.application.localeDropdown.it-IT": "İtalyanca",
"app.submenu.application.localeDropdown.ja": "Japonca",
"app.submenu.application.localeDropdown.ka": "Gürcüce",
"app.submenu.application.localeDropdown.km": "Kmer",
"app.submenu.application.localeDropdown.kn": "Kannada",
"app.submenu.application.localeDropdown.ko-KR": "Korece",
"app.submenu.application.localeDropdown.lo-LA": "Lao",
"app.submenu.application.localeDropdown.lt-LT": "Litvanca",
"app.submenu.application.localeDropdown.lv": "Letonca",
"app.submenu.application.localeDropdown.ml": "Malayalam",
"app.submenu.application.localeDropdown.mn-MN": "Moğolca",
"app.submenu.application.localeDropdown.nb-NO": "Norveçce (bokmal)",
"app.submenu.application.localeDropdown.nl": "Felemenkçe",
"app.submenu.application.localeDropdown.oc": "Oksitan",
"app.submenu.application.localeDropdown.pl-PL": "Lehçe",
"app.submenu.application.localeDropdown.pt": "Portekizce",
"app.submenu.application.localeDropdown.pt-BR": "Portekizce (Brezilya)",
"app.submenu.application.localeDropdown.ro-RO": "Romence",
"app.submenu.application.localeDropdown.ru": "Rusça",
"app.submenu.application.localeDropdown.sk-SK": "Slovakça",
"app.submenu.application.localeDropdown.sl": "Slovence",
"app.submenu.application.localeDropdown.sr": "Sırça",
"app.submenu.application.localeDropdown.sv-SE": "İsveççe",
"app.submenu.application.localeDropdown.ta": "Tamil",
"app.submenu.application.localeDropdown.te": "Telugu",
"app.submenu.application.localeDropdown.th": "Tay",
"app.submenu.application.localeDropdown.tr": "Türkçe",
"app.submenu.application.localeDropdown.tr-TR": "Türkçe (Türkiye)",
"app.submenu.application.localeDropdown.uk-UA": "Ukraynaca",
"app.submenu.application.localeDropdown.vi": "Vietnamca",
"app.submenu.application.localeDropdown.vi-VN": "Vietnamca (Vietnam)",
"app.submenu.application.localeDropdown.zh-CN": "Çince basit (Çin)",
"app.submenu.application.localeDropdown.zh-TW": "Çince geleneksel (Tayvan)",
"app.submenu.notification.SectionTitle": "Bildirimler",
"app.submenu.notification.Desc": "Neyin nasıl bildirileceğini belirleyin.",
"app.submenu.notification.audioAlertLabel": "Sesli uyarılar",

View File

@ -266,6 +266,7 @@
"app.presentationUploder.rejectedError": "Неможливо завантажити вибрані файл(и). Перевірте тип файлу(iв).",
"app.presentationUploder.connectionClosedError": "Перервано через поганий зв'язок. Спробуйте пізніше",
"app.presentationUploder.upload.progress": "Завантаження ({0}%)",
"app.presentationUploder.conversion.204": "Відсутній вміст для перетворення",
"app.presentationUploder.upload.413": "Файл надто великий, розмір перевищує допустимі {0} МБ",
"app.presentationUploder.genericError": "Йой! Щось пішло не так ...",
"app.presentationUploder.upload.408": "Вичерпано час запиту дії токену для завантаження.",
@ -512,10 +513,10 @@
"app.submenu.application.localeDropdown.te": "Телугу",
"app.submenu.application.localeDropdown.th": "Тайська",
"app.submenu.application.localeDropdown.tr": "Турецька",
"app.submenu.application.localeDropdown.tr-TR": " Турецька",
"app.submenu.application.localeDropdown.tr-TR": "Турецька (Туреччина)",
"app.submenu.application.localeDropdown.uk-UA": "Українська",
"app.submenu.application.localeDropdown.vi": "В'єтнамська",
"app.submenu.application.localeDropdown.vi-VN": " Турецька",
"app.submenu.application.localeDropdown.vi-VN": "В’єтнамська (В’єтнам)",
"app.submenu.application.localeDropdown.zh-CN": "Китайська спрощена (Китай)",
"app.submenu.application.localeDropdown.zh-TW": "Китайська спрощена (Тайвань)",
"app.submenu.notification.SectionTitle": "Сповіщення",

View File

@ -119,5 +119,21 @@ test.describe.parallel('Breakout', () => {
await join.create();
await join.moveUserToOtherRoom();
});
test('Export breakout room shared notes', async ({ browser, context, page }) => {
const join = new Join(browser, context);
await join.initPages(page);
await join.create(true); // capture breakout notes
await join.exportBreakoutNotes();
});
// temporarily skipped until the following issue gets resolved:
// https://github.com/bigbluebutton/bigbluebutton/issues/16368
test.skip('Export breakout room whiteboard annotations', async ({ browser, context, page }) => {
const join = new Join(browser, context);
await join.initPages(page);
await join.create(false, true); // capture breakout whiteboard
await join.exportBreakoutWhiteboard();
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -9,12 +9,15 @@ class Create extends MultiUsers {
}
// Create Breakoutrooms
async create() {
async create(captureNotes = false, captureWhiteboard = false) {
await this.modPage.waitAndClick(e.manageUsers);
await this.modPage.waitAndClick(e.createBreakoutRooms);
//Randomly assignment
await this.modPage.waitAndClick(e.randomlyAssign);
if(captureNotes) await this.modPage.page.check(e.captureBreakoutSharedNotes);
if(captureWhiteboard) await this.modPage.page.check(e.captureBreakoutWhiteboard);
await this.modPage.waitAndClick(e.modalConfirmButton, ELEMENT_WAIT_LONGER_TIME);
await this.userPage.hasElement(e.modalConfirmButton);

View File

@ -1,3 +1,4 @@
const { default: test } = require('@playwright/test');
const { Create } = require('./create');
const utilScreenShare = require('../screenshare/util');
const e = require('../core/elements');
@ -5,6 +6,7 @@ const { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } = require('../core/constan
const { getSettings } = require('../core/settings');
const { expect } = require('@playwright/test');
const { sleep } = require('../core/helpers');
const { getNotesLocator } = require('../sharednotes/util');
class Join extends Create {
constructor(browser, context) {
@ -140,6 +142,92 @@ class Join extends Create {
await this.userPage.waitAndClick(e.modalConfirmButton);
await this.modPage.hasText(e.userNameBreakoutRoom2, /Attendee/);
}
async exportBreakoutNotes() {
const { sharedNotesEnabled } = getSettings();
test.fail(!sharedNotesEnabled, 'Shared notes is disabled');
const breakoutUserPage = await this.joinRoom();
await breakoutUserPage.hasElement(e.presentationTitle);
await breakoutUserPage.waitAndClick(e.sharedNotes);
await breakoutUserPage.waitForSelector(e.hideNotesLabel);
const notesLocator = getNotesLocator(breakoutUserPage);
await notesLocator.type(e.message);
await sleep(1000); // making sure there's enough time for the typing to finish
await this.modPage.waitAndClick(e.breakoutRoomsItem);
await this.modPage.waitAndClick(e.breakoutOptionsMenu);
await this.modPage.waitAndClick(e.endAllBreakouts);
await this.modPage.hasElement(e.presentationUploadProgressToast);
await this.modPage.page.waitForSelector(e.presentationUploadProgressToast, { state: 'detached' });
await this.modPage.waitAndClick(e.closeModal); // closing the audio modal
await this.modPage.waitAndClick(e.actions);
await this.modPage.checkElementCount(e.actionsItem, 9);
await this.modPage.getLocatorByIndex(e.actionsItem, 1).click();
const wb = await this.modPage.page.$(e.whiteboard);
const wbBox = await wb.boundingBox();
const clipObj = {
x: wbBox.x,
y: wbBox.y,
width: wbBox.width,
height: wbBox.height,
};
await expect(this.modPage.page).toHaveScreenshot('capture-breakout-notes.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
}
async exportBreakoutWhiteboard() {
const { sharedNotesEnabled } = getSettings();
test.fail(!sharedNotesEnabled, 'Shared notes is disabled');
const breakoutUserPage = await this.joinRoom();
await breakoutUserPage.hasElement(e.presentationTitle);
await breakoutUserPage.waitAndClick(e.sharedNotes);
await breakoutUserPage.waitForSelector(e.hideNotesLabel);
// draw a line
await breakoutUserPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await breakoutUserPage.waitAndClick(e.wbShapesButton);
await breakoutUserPage.waitAndClick(e.wbLineShape);
const wbBreakout = await breakoutUserPage.page.$(e.whiteboard);
const wbBoxBreakout = await wbBreakout.boundingBox();
await breakoutUserPage.page.mouse.move(wbBoxBreakout.x + 0.3 * wbBoxBreakout.width, wbBoxBreakout.y + 0.3 * wbBoxBreakout.height);
await breakoutUserPage.page.mouse.down();
await breakoutUserPage.page.mouse.move(wbBoxBreakout.x + 0.7 * wbBoxBreakout.width, wbBoxBreakout.y + 0.7 * wbBoxBreakout.height);
await breakoutUserPage.page.mouse.up();
await sleep(1000); // making sure there's enough time for the typing to finish
await this.modPage.waitAndClick(e.breakoutRoomsItem);
await this.modPage.waitAndClick(e.breakoutOptionsMenu);
await this.modPage.waitAndClick(e.endAllBreakouts);
await this.modPage.waitForSelector(e.presentationUploadProgressToast, ELEMENT_WAIT_LONGER_TIME);
await this.modPage.page.waitForSelector(e.presentationUploadProgressToast, { state: 'detached' });
await this.modPage.waitAndClick(e.closeModal); // closing the audio modal
await this.modPage.waitAndClick(e.actions);
await this.modPage.checkElementCount(e.actionsItem, 9);
await this.modPage.getLocatorByIndex(e.actionsItem, 1).click();
const wbMod = await this.modPage.page.$(e.whiteboard);
const wbBoxMod = await wbMod.boundingBox();
const clipObj = {
x: wbBoxMod.x,
y: wbBoxMod.y,
width: wbBoxMod.width,
height: wbBoxMod.height,
};
await expect(this.modPage.page).toHaveScreenshot('capture-breakout-whiteboard.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
}
}
exports.Join = Join;

View File

@ -1,5 +1,6 @@
// Common
exports.actions = 'button[data-test="actionsButton"]';
exports.actionsItem = 'div[id="actions-dropdown-menu"] ul li';
exports.pollMenuButton = 'div[data-test="pollMenuButton"]';
exports.optionsButton = 'button[data-test="optionsButton"]';
exports.settings = 'li[data-test="settings"]';
@ -82,6 +83,8 @@ exports.endAllBreakouts = 'li[data-test="endAllBreakouts"]';
exports.breakoutRoomList = 'div[data-test="breakoutRoomList"]';
exports.warningNoUserAssigned = 'span[data-test="warningNoUserAssigned"]';
exports.timeRemaining = 'span[data-test="timeRemaining"]';
exports.captureBreakoutSharedNotes = 'input[id="captureNotesBreakoutCheckbox"]';
exports.captureBreakoutWhiteboard = 'input[id="captureSlidesBreakoutCheckbox"]';
// Chat
exports.chatBox = 'textarea[id="message-input"]';
@ -345,6 +348,13 @@ exports.lockPrivateChat = 'input[data-test="lockPrivateChat"]';
exports.lockEditSharedNotes = 'input[data-test="lockEditSharedNotes"]';
exports.lockUserList = 'input[data-test="lockUserList"]';
// Closed Captions
exports.writeClosedCaptions = 'li[data-test="writeClosedCaptions"]';
exports.startWritingClosedCaptions = 'button[data-test="startWritingClosedCaptions"]';
exports.startViewingClosedCaptionsBtn = 'button[data-test="startViewingClosedCaptionsBtn"]';
exports.startViewingClosedCaptions = 'button[data-test="startViewingClosedCaptions"]';
exports.liveCaptions = 'div[data-test="liveCaptions"]';
// Locales
exports.locales = ['af', 'ar', 'az', 'bg-BG', 'bn', 'ca', 'cs-CZ', 'da', 'de',
'dv', 'el-GR', 'en', 'eo', 'es', 'es-419', 'es-ES', 'es-MX', 'et', 'eu',
@ -377,7 +387,9 @@ exports.wbRectangleShape = 'span[id="TD-PrimaryTools-Shapes-rectangle"]';
exports.wbEllipseShape = 'span[id="TD-PrimaryTools-Shapes-ellipse"]';
exports.wbTriangleShape = 'span[id="TD-PrimaryTools-Shapes-triangle"]';
exports.wbLineShape = 'span[id="TD-PrimaryTools-Shapes-line"]';
exports.wbPencilShape = 'span[id="TD-PrimaryTools-Pencil"]';
exports.wbPencilShape = 'button[id="TD-PrimaryTools-Pencil"]';
exports.wbStickyNoteShape = 'button[id="TD-PrimaryTools-Pencil2"]';
exports.wbTextShape = 'button[id="TD-PrimaryTools-Text"]';
exports.wbTypedText = 'div[data-shape="text"]';
exports.wbDrawnRectangle = 'div[data-shape="rectangle"]';
exports.wbDrawnLine = 'div[data-shape="draw"]';

View File

@ -5,6 +5,7 @@ const e = require('../core/elements');
const { waitAndClearDefaultPresentationNotification } = require('../notifications/util');
const { sleep } = require('../core/helpers');
const { checkAvatarIcon, checkIsPresenter, checkMutedUsers } = require('./util');
const { getNotesLocator } = require('../sharednotes/util');
const { checkTextContent } = require('../core/util');
const { getSettings } = require('../core/settings');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
@ -265,6 +266,27 @@ class MultiUsers {
await this.modPage.hasElement(e.multiUsersWhiteboardOn);
}
async writeClosedCaptions() {
await this.modPage.waitForSelector(e.whiteboard);
await this.modPage2.waitForSelector(e.whiteboard);
await this.modPage.waitAndClick(e.manageUsers);
await this.modPage.waitAndClick(e.writeClosedCaptions);
await this.modPage.waitAndClick(e.startWritingClosedCaptions);
await this.modPage.waitAndClick(e.startViewingClosedCaptionsBtn);
await this.modPage2.waitAndClick(e.startViewingClosedCaptionsBtn);
await this.modPage.waitAndClick(e.startViewingClosedCaptions);
await this.modPage2.waitAndClick(e.startViewingClosedCaptions);
const notesLocator = getNotesLocator(this.modPage);
await notesLocator.type(e.message);
await this.modPage.hasText(e.liveCaptions, e.message);
await this.modPage2.hasText(e.liveCaptions, e.message);
}
}
exports.MultiUsers = MultiUsers;

View File

@ -226,6 +226,13 @@ test.describe.parallel('User', () => {
await multiusers.initUserPage(false);
await multiusers.muteAllUsersExceptPresenter();
});
test('Write closed captions', async ({ browser, context, page }) => {
const multiusers = new MultiUsers(browser, context);
await multiusers.initModPage(page, true);
await multiusers.initModPage2(true);
await multiusers.writeClosedCaptions();
});
});
test.describe.parallel('Mobile devices', () => {

View File

@ -0,0 +1,45 @@
const { expect } = require('@playwright/test');
const Page = require('../core/page');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
class DrawPencil extends MultiUsers {
constructor(browser, context) {
super(browser, context);
}
async test() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await this.modPage.waitAndClick(e.wbPencilShape);
const wb = await this.modPage.page.$(e.whiteboard);
const wbBox = await wb.boundingBox();
const moveOptions = { steps: 50 }; // to slow down
await this.modPage.page.mouse.move(wbBox.x + 0.2 * wbBox.width, wbBox.y + 0.2 * wbBox.height);
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.4 * wbBox.width, wbBox.y + 0.4 * wbBox.height, moveOptions);
await this.modPage.page.mouse.move(wbBox.x + 0.6 * wbBox.width, wbBox.y + 0.2 * wbBox.height, moveOptions);
await this.modPage.page.mouse.move(wbBox.x + 0.8 * wbBox.width, wbBox.y + 0.4 * wbBox.height, moveOptions);
await this.modPage.page.mouse.up();
const clipObj = {
x: wbBox.x,
y: wbBox.y,
width: wbBox.width,
height: wbBox.height,
};
await expect(this.modPage.page).toHaveScreenshot('moderator1-pencil.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-pencil.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
}
}
exports.DrawPencil = DrawPencil;

View File

@ -0,0 +1,47 @@
const { expect } = require('@playwright/test');
const Page = require('../core/page');
const e = require('../core/elements');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { MultiUsers } = require('../user/multiusers');
class DrawStickyNote extends MultiUsers {
constructor(browser, context) {
super(browser, context);
}
async test() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await this.modPage.waitAndClick(e.wbStickyNoteShape);
const wb = await this.modPage.page.$(e.whiteboard);
const wbBox = await wb.boundingBox();
await this.modPage.page.mouse.click(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height);
await this.modPage.press('A');
await this.modPage.press('A');
await this.modPage.press('Backspace');
await this.modPage.press('B');
await this.modPage.page.mouse.click(wbBox.x + 0.6 * wbBox.width, wbBox.y + 0.6 * wbBox.height);
await this.modPage.hasText(e.wbTypedText, 'AB');
await this.modPage2.hasText(e.wbTypedText, 'AB');
const clipObj = {
x: wbBox.x,
y: wbBox.y,
width: wbBox.width,
height: wbBox.height,
};
await expect(this.modPage.page).toHaveScreenshot('moderator1-sticky.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
await expect(this.modPage2.page).toHaveScreenshot('moderator2-sticky.png', {
maxDiffPixels: 1000,
clip: clipObj,
});
}
}
exports.DrawStickyNote = DrawStickyNote;

Some files were not shown because too many files have changed in this diff Show More