Merge tag 'v2.5.1' into merge25-26-jun16

This commit is contained in:
Ramon Souza 2022-06-16 13:58:54 -03:00
commit edeb70de0d
116 changed files with 1709 additions and 1265 deletions

View File

@ -20,7 +20,7 @@ trait CreateDefaultPublicGroupChat {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(GroupChatCreatedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(GroupChatCreatedEvtMsg.NAME, meetingId, userId)
val body = GroupChatCreatedEvtMsgBody(correlationId, gc.id, gc.createdBy, gc.name, gc.access, gc.users, msgs)
val body = GroupChatCreatedEvtMsgBody(correlationId, gc.id, gc.createdBy, gc.access, gc.users, msgs)
val event = GroupChatCreatedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}

View File

@ -62,7 +62,7 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
}
}
val gc = GroupChatApp.createGroupChat(msg.body.name, msg.body.access, createdBy, users, msgs)
val gc = GroupChatApp.createGroupChat(msg.body.access, createdBy, users, msgs)
sendMessages(msg, gc, liveMeeting, bus)
val groupChats = state.groupChats.add(gc)
@ -84,12 +84,12 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
BbbCoreEnvelope(name, routing)
}
def makeBody(chatId: String, name: String,
def makeBody(chatId: String,
access: String, correlationId: String,
createdBy: GroupChatUser, users: Vector[GroupChatUser],
msgs: Vector[GroupChatMsgToUser]): GroupChatCreatedEvtMsgBody = {
GroupChatCreatedEvtMsgBody(correlationId, chatId, createdBy,
name, access, users, msgs)
access, users, msgs)
}
val meetingId = liveMeeting.props.meetingProp.intId
@ -102,7 +102,7 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
val envelope = makeEnvelope(MessageTypes.DIRECT, GroupChatCreatedEvtMsg.NAME, meetingId, userId)
val header = makeHeader(GroupChatCreatedEvtMsg.NAME, meetingId, userId)
val body = makeBody(gc.id, gc.name, gc.access, correlationId, gc.createdBy, users, msgs)
val body = makeBody(gc.id, gc.access, correlationId, gc.createdBy, users, msgs)
val event = GroupChatCreatedEvtMsg(header, body)
val outEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(outEvent)
@ -117,7 +117,7 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
meetingId, userId)
val header = makeHeader(GroupChatCreatedEvtMsg.NAME, meetingId, userId)
val body = makeBody(gc.id, gc.name, gc.access, correlationId, gc.createdBy, users, msgs)
val body = makeBody(gc.id, gc.access, correlationId, gc.createdBy, users, msgs)
val event = GroupChatCreatedEvtMsg(header, body)
val outEvent = BbbCommonEnvCoreMsg(envelope, event)

View File

@ -27,8 +27,8 @@ trait GetGroupChatsReqMsgHdlr {
val publicChats = state.groupChats.findAllPublicChats()
val privateChats = state.groupChats.findAllPrivateChatsForUser(msg.header.userId)
val pubChats = publicChats map (pc => GroupChatInfo(pc.id, pc.name, pc.access, pc.createdBy, pc.users))
val privChats = privateChats map (pc => GroupChatInfo(pc.id, pc.name, pc.access, pc.createdBy, pc.users))
val pubChats = publicChats map (pc => GroupChatInfo(pc.id, pc.access, pc.createdBy, pc.users))
val privChats = privateChats map (pc => GroupChatInfo(pc.id, pc.access, pc.createdBy, pc.users))
val allChats = pubChats ++ privChats

View File

@ -9,10 +9,10 @@ object GroupChatApp {
val MAIN_PUBLIC_CHAT = "MAIN-PUBLIC-GROUP-CHAT"
def createGroupChat(chatName: String, access: String, createBy: GroupChatUser,
def createGroupChat(access: String, createBy: GroupChatUser,
users: Vector[GroupChatUser], msgs: Vector[GroupChatMessage]): GroupChat = {
val gcId = GroupChatFactory.genId()
GroupChatFactory.create(gcId, chatName, access, createBy, users, msgs)
GroupChatFactory.create(gcId, access, createBy, users, msgs)
}
def toGroupChatMessage(sender: GroupChatUser, msg: GroupChatMsgFromUser): GroupChatMessage = {
@ -46,13 +46,15 @@ object GroupChatApp {
def createDefaultPublicGroupChat(): GroupChat = {
val createBy = GroupChatUser(SystemUser.ID)
GroupChatFactory.create(MAIN_PUBLIC_CHAT, MAIN_PUBLIC_CHAT, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
GroupChatFactory.create(MAIN_PUBLIC_CHAT, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
}
def createTestPublicGroupChat(state: MeetingState2x): MeetingState2x = {
val createBy = GroupChatUser(SystemUser.ID)
val defaultPubGroupChat = GroupChatFactory.create("TEST_GROUP_CHAT", "TEST_GROUP_CHAT",
GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
val defaultPubGroupChat = GroupChatFactory.create(
"TEST_GROUP_CHAT",
GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty
)
val groupChats = state.groupChats.add(defaultPubGroupChat)
state.update(groupChats)
}

View File

@ -1,6 +1,7 @@
package org.bigbluebutton.core.apps.groupchats
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.PermissionCheck
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting }
@ -43,17 +44,27 @@ trait SendGroupChatMessageMsgHdlr extends HandlerHelpers {
sender <- GroupChatApp.findGroupChatUser(msg.header.userId, liveMeeting.users2x)
chat <- state.groupChats.find(msg.body.chatId)
} yield {
val gcm = GroupChatApp.toGroupChatMessage(sender, msg.body.msg)
val gcs = GroupChatApp.addGroupChatMessage(chat, state.groupChats, gcm)
val chatIsPrivate = chat.access == GroupChatAccess.PRIVATE;
val userIsAParticipant = chat.users.filter(u => u.id == sender.id).length > 0;
val event = buildGroupChatMessageBroadcastEvtMsg(
liveMeeting.props.meetingProp.intId,
msg.header.userId, msg.body.chatId, gcm
)
if ((chatIsPrivate && userIsAParticipant) || !chatIsPrivate) {
val gcm = GroupChatApp.toGroupChatMessage(sender, msg.body.msg)
val gcs = GroupChatApp.addGroupChatMessage(chat, state.groupChats, gcm)
bus.outGW.send(event)
val event = buildGroupChatMessageBroadcastEvtMsg(
liveMeeting.props.meetingProp.intId,
msg.header.userId, msg.body.chatId, gcm
)
bus.outGW.send(event)
state.update(gcs)
} else {
val reason = "User isn't a participant of the chat"
PermissionCheck.ejectUserForFailedPermission(msg.header.meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
state
}
state.update(gcs)
}
newState match {

View File

@ -41,7 +41,7 @@ trait SyncGetGroupChatsInfoMsgHdlr {
val respMsg = buildSyncGetGroupChatMsgsRespMsg(msgs, pc.id)
bus.outGW.send(respMsg)
GroupChatInfo(pc.id, pc.name, pc.access, pc.createdBy, pc.users)
GroupChatInfo(pc.id, pc.access, pc.createdBy, pc.users)
})
// publishing a message with the group chat info

View File

@ -51,7 +51,7 @@ trait EjectUserFromMeetingCmdMsgHdlr extends RightsManagementTrait {
breakoutModel <- state.breakout
} yield {
breakoutModel.rooms.values.foreach { room =>
room.users.filter(u => u.id == ru.externId + "-" + room.sequence).foreach(user => {
room.users.filter(u => u.id == ru.id + "-" + room.sequence).foreach(user => {
eventBus.publish(BigBlueButtonEvent(room.id, EjectUserFromBreakoutInternalMsg(meetingId, room.id, user.id, ejectedBy, reason, EjectReasonCode.EJECT_USER, ban)))
})
}

View File

@ -5,9 +5,9 @@ import org.bigbluebutton.core.util.RandomStringGenerator
object GroupChatFactory {
def genId(): String = System.currentTimeMillis() + "-" + RandomStringGenerator.randomAlphanumericString(8)
def create(id: String, name: String, access: String, createdBy: GroupChatUser,
def create(id: String, access: String, createdBy: GroupChatUser,
users: Vector[GroupChatUser], msgs: Vector[GroupChatMessage]): GroupChat = {
new GroupChat(id, name, access, createdBy, users, msgs)
new GroupChat(id, access, createdBy, users, msgs)
}
}
@ -23,7 +23,7 @@ case class GroupChats(chats: collection.immutable.Map[String, GroupChat]) {
def getAllGroupChatsInMeeting(): Vector[GroupChat] = chats.values.toVector
}
case class GroupChat(id: String, name: String, access: String, createdBy: GroupChatUser,
case class GroupChat(id: String, access: String, createdBy: GroupChatUser,
users: Vector[GroupChatUser],
msgs: Vector[GroupChatMessage]) {
def findMsgWithId(id: String): Option[GroupChatMessage] = msgs.find(m => m.id == id)

View File

@ -7,10 +7,9 @@ class GroupsChatTests extends UnitSpec {
"A GroupChat" should "be able to add and remove user" in {
val gcId = "gc-id"
val chatName = "Public"
val userId = "uid-1"
val createBy = GroupChatUser("groupId")
val gc = GroupChatFactory.create(gcId, chatName, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
val gc = GroupChatFactory.create(gcId, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
val user = GroupChatUser(userId)
val gc2 = gc.add(user)
assert(gc2.users.size == 1)
@ -26,8 +25,7 @@ class GroupsChatTests extends UnitSpec {
"A GroupChat" should "be able to add, update, and remove msg" in {
val createBy = GroupChatUser("groupId")
val gcId = "gc-id"
val chatName = "Public"
val gc = GroupChatFactory.create(gcId, chatName, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
val gc = GroupChatFactory.create(gcId, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
val msgId1 = "msgid-1"
val ts = System.currentTimeMillis()
val hello = "Hello World!"

View File

@ -18,7 +18,7 @@ val compileSettings = Seq(
"-Xlint",
"-Ywarn-dead-code",
"-language:_",
"-target:jvm-1.8",
"-target:jvm-1.11",
"-encoding", "UTF-8"
),
javacOptions ++= List(

View File

@ -8,7 +8,7 @@ object GroupChatAccess {
case class GroupChatUser(id: String, name: String = "", role: String = "VIEWER")
case class GroupChatMsgFromUser(correlationId: String, sender: GroupChatUser, chatEmphasizedText: Boolean = false, message: String)
case class GroupChatMsgToUser(id: String, timestamp: Long, correlationId: String, sender: GroupChatUser, chatEmphasizedText: Boolean = false, message: String)
case class GroupChatInfo(id: String, name: String, access: String, createdBy: GroupChatUser, users: Vector[GroupChatUser])
case class GroupChatInfo(id: String, access: String, createdBy: GroupChatUser, users: Vector[GroupChatUser])
object OpenGroupChatWindowReqMsg { val NAME = "OpenGroupChatWindowReqMsg" }
case class OpenGroupChatWindowReqMsg(header: BbbClientMsgHeader, body: OpenGroupChatWindowReqMsgBody) extends StandardMsg
@ -36,14 +36,14 @@ case class GetGroupChatMsgsRespMsgBody(chatId: String, msgs: Vector[GroupChatMsg
object CreateGroupChatReqMsg { val NAME = "CreateGroupChatReqMsg" }
case class CreateGroupChatReqMsg(header: BbbClientMsgHeader, body: CreateGroupChatReqMsgBody) extends StandardMsg
case class CreateGroupChatReqMsgBody(correlationId: String, name: String, access: String,
case class CreateGroupChatReqMsgBody(correlationId: String, access: String,
users: Vector[String], msg: Vector[GroupChatMsgFromUser])
object GroupChatCreatedEvtMsg { val NAME = "GroupChatCreatedEvtMsg" }
case class GroupChatCreatedEvtMsg(header: BbbClientMsgHeader, body: GroupChatCreatedEvtMsgBody) extends BbbCoreMsg
case class GroupChatCreatedEvtMsgBody(correlationId: String, chatId: String, createdBy: GroupChatUser,
name: String, access: String,
users: Vector[GroupChatUser], msg: Vector[GroupChatMsgToUser])
access: String,
users: Vector[GroupChatUser], msg: Vector[GroupChatMsgToUser])
object DestroyGroupChatReqMsg { val NAME = "DestroyGroupChatReqMsg" }
case class DestroyGroupChatReqMsg(header: BbbClientMsgHeader, body: DestroyGroupChatReqMsgBody) extends StandardMsg

View File

@ -6251,11 +6251,11 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"node_modules/ejs": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz",
"integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==",
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz",
"integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==",
"dependencies": {
"jake": "^10.6.1"
"jake": "^10.8.5"
},
"bin": {
"ejs": "bin/cli.js"
@ -21236,9 +21236,9 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"ejs": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.7.tgz",
"integrity": "sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw==",
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz",
"integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==",
"requires": {
"jake": "^10.8.5"
}
@ -22007,29 +22007,11 @@
}
},
"filelist": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.3.tgz",
"integrity": "sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
"integrity": "sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==",
"requires": {
"minimatch": "^5.0.1"
},
"dependencies": {
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"requires": {
"balanced-match": "^1.0.0"
}
},
"minimatch": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
"integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
"requires": {
"brace-expansion": "^2.0.1"
}
}
"minimatch": "^3.0.4"
}
},
"filesize": {

View File

@ -1 +1 @@
git clone --branch v1.2.0 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads
git clone --branch v1.2.1 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads

View File

@ -1 +1 @@
git clone --branch v2.3.0 --depth 1 https://github.com/bigbluebutton/bbb-webhooks bbb-webhooks
git clone --branch v2.6.0 --depth 1 https://github.com/bigbluebutton/bbb-webhooks bbb-webhooks

View File

@ -22,7 +22,6 @@ export default function createGroupChat(receiver) {
msg: [],
users: [receiver.userId],
access: CHAT_ACCESS_PRIVATE,
name: receiver.name,
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);

View File

@ -9,7 +9,6 @@ export default function addGroupChat(meetingId, chat) {
id: Match.Maybe(String),
chatId: Match.Maybe(String),
correlationId: Match.Maybe(String),
name: String,
access: String,
createdBy: Object,
users: Array,
@ -19,9 +18,8 @@ export default function addGroupChat(meetingId, chat) {
const chatDocument = {
meetingId,
chatId: chat.chatId || chat.id,
name: chat.name,
access: chat.access,
users: chat.users.map(u => u.id),
users: chat.users.map((u) => u.id),
participants: chat.users,
createdBy: chat.createdBy.id,
};
@ -39,9 +37,9 @@ export default function addGroupChat(meetingId, chat) {
const { insertedId } = GroupChat.upsert(selector, modifier);
if (insertedId) {
Logger.info(`Added group-chat name=${chat.name} meetingId=${meetingId}`);
Logger.info(`Added group-chat chatId=${chatDocument.chatId} meetingId=${meetingId}`);
} else {
Logger.info(`Upserted group-chat name=${chat.name} meetingId=${meetingId}`);
Logger.info(`Upserted group-chat chatId=${chatDocument.chatId} meetingId=${meetingId}`);
}
} catch (err) {
Logger.error(`Adding group-chat to collection: ${err}`);

View File

@ -4,6 +4,7 @@ import Users from '/imports/api/users';
import userJoin from './userJoin';
import pendingAuthenticationsStore from '../store/pendingAuthentications';
import createDummyUser from '../modifiers/createDummyUser';
import updateUserConnectionId from '../modifiers/updateUserConnectionId';
import ClientConnections from '/imports/startup/server/ClientConnections';
import upsertValidationState from '/imports/api/auth-token-validation/server/modifiers/upsertValidationState';
@ -81,6 +82,8 @@ export default function handleValidateAuthToken({ body }, meetingId) {
if (!User) {
createDummyUser(meetingId, userId, authToken);
}else{
updateUserConnectionId(meetingId, userId, methodInvocationObject.connection.id);
}
ClientConnections.add(sessionId, methodInvocationObject.connection);

View File

@ -0,0 +1,32 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
export default function updateUserConnectionId(meetingId, userId, connectionId) {
check(meetingId, String);
check(userId, String);
check(connectionId, String);
const selector = { meetingId, userId };
const modifier = {
$set: {
currentConnectionId: connectionId,
connectionIdUpdateTime: new Date().getTime(),
},
};
const User = Users.findOne(selector);
if (User) {
try {
const updated = Users.update(selector, modifier);
if (updated) {
Logger.info(`Updated connection user=${userId} connectionid=${connectionId} meeting=${meetingId}`);
}
} catch (err) {
Logger.error(`Updating user connection: ${err}`);
}
}
}

View File

@ -22,7 +22,7 @@ export default function handleVoiceUpdate({ body }, meetingId) {
const isDialInUser = (userId, meetingID) => !!Users.findOne({ meetingId: meetingID, userId, clientType: 'dial-in-user' });
// if the user is dial-in, leaving voice also means leaving userlist
if (isDialInUser(voiceUserId, meetingId)) removeUser(meetingId, intId);
if (isDialInUser(voiceUserId, meetingId)) removeUser(voiceUser, meetingId);
return removeVoiceUser(meetingId, voiceUser);
}

View File

@ -353,6 +353,8 @@ export default withTracker(() => {
userId: 1,
inactivityCheck: 1,
responseDelay: 1,
currentConnectionId: 1,
connectionIdUpdateTime: 1,
};
const User = Users.findOne({ intId: credentials.requesterUserId }, { fields });
const meeting = Meetings.findOne({ meetingId }, {
@ -371,6 +373,14 @@ export default withTracker(() => {
const ejected = User?.ejected;
const ejectedReason = User?.ejectedReason;
const meetingEndedReason = meeting?.meetingEndedReason;
const currentConnectionId = User?.currentConnectionId;
const { connectionID, connectionAuthTime } = Auth;
const connectionIdUpdateTime = User?.connectionIdUpdateTime;
if (currentConnectionId && currentConnectionId !== connectionID && connectionIdUpdateTime > connectionAuthTime) {
Session.set('codeError', 403);
Session.set('errorMessageDescription', 'joined_another_window_reason')
}
let userSubscriptionHandler;

View File

@ -284,6 +284,7 @@ class ActionsDropdown extends PureComponent {
isMeteorConnected,
isDropdownOpen,
isMobile,
isRTL,
} = this.props;
const availableActions = this.getAvailableActions();
@ -296,7 +297,7 @@ class ActionsDropdown extends PureComponent {
|| !isMeteorConnected) {
return null;
}
const customStyles = { top: '-3rem' };
const customStyles = { top: '-1rem' };
return (
<BBBMenu
@ -324,8 +325,8 @@ class ActionsDropdown extends PureComponent {
elevation: 3,
getContentAnchorEl: null,
fullwidth: "true",
anchorOrigin: { vertical: 'top', horizontal: 'left' },
transformorigin: { vertical: 'top', horizontal: 'left' },
anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
}}
/>
);

View File

@ -4,7 +4,7 @@ import Presentations from '/imports/api/presentations';
import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service';
import PresentationPodService from '/imports/ui/components/presentation-pod/service';
import ActionsDropdown from './component';
import { layoutSelectInput, layoutDispatch } from '../../layout/context';
import { layoutSelectInput, layoutDispatch, layoutSelect } from '../../layout/context';
import getFromUserSettings from '/imports/ui/services/users-settings';
import { SMALL_VIEWPORT_BREAKPOINT } from '../../layout/enums';
@ -14,6 +14,7 @@ const ActionsDropdownContainer = (props) => {
const { width: browserWidth } = layoutSelectInput((i) => i.browser);
const isMobile = browserWidth <= SMALL_VIEWPORT_BREAKPOINT;
const layoutContextDispatch = layoutDispatch();
const isRTL = layoutSelect((i) => i.isRTL);
return (
<ActionsDropdown {...{
@ -21,6 +22,7 @@ const ActionsDropdownContainer = (props) => {
sidebarContent,
sidebarNavigation,
isMobile,
isRTL,
...props,
}}
/>

View File

@ -23,6 +23,7 @@ import ManyWebcamsNotifier from '/imports/ui/components/video-provider/many-user
import UploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
import CaptionsSpeechContainer from '/imports/ui/components/captions/speech/container';
import RandomUserSelectContainer from '/imports/ui/components/common/modal/random-user/container';
import ScreenReaderAlertContainer from '../screenreader-alert/container';
import NewWebcamContainer from '../webcam/container';
import PresentationAreaContainer from '../presentation/presentation-area/container';
import ScreenshareContainer from '../screenshare/container';
@ -609,6 +610,7 @@ class App extends Component {
>
{this.renderActivityCheck()}
{this.renderUserInformation()}
<ScreenReaderAlertContainer />
<BannerBarContainer />
<NotificationsBarContainer />
<SidebarNavigationContainer />

View File

@ -14,6 +14,7 @@ import {
setUserSelectedMicrophone,
setUserSelectedListenOnly,
} from '../audio-modal/service';
import { layoutSelect } from '/imports/ui/components/layout/context';
import Service from '../service';
import AppService from '/imports/ui/components/app/service';
@ -25,7 +26,8 @@ const AudioControlsContainer = (props) => {
const {
users, lockSettings, userLocks, children, ...newProps
} = props;
return <AudioControls {...newProps} />;
const isRTL = layoutSelect((i) => i.isRTL);
return <AudioControls {...{ ...newProps, isRTL }} />;
};
const handleLeaveAudio = () => {

View File

@ -346,6 +346,7 @@ class InputStreamLiveSelector extends Component {
currentInputDeviceId,
currentOutputDeviceId,
isListenOnly,
isRTL,
} = this.props;
const inputDeviceList = !isListenOnly
@ -389,10 +390,21 @@ class InputStreamLiveSelector extends Component {
label={intl.formatMessage(intlMessages.changeAudioDevice)}
hideLabel
tabIndex={0}
rotate
/>
</>
)}
actions={dropdownListComplete}
opts={{
id: 'default-dropdown-menu',
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getContentAnchorEl: null,
fullwidth: 'true',
anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'left' : 'right' },
transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
}}
/>
);
}

View File

@ -8,6 +8,7 @@ import {
} from '/imports/ui/stylesheets/styled-components/palette';
import {
mdPaddingY,
btnSpacing,
} from '/imports/ui/stylesheets/styled-components/general';
import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
@ -48,6 +49,8 @@ const AudioModalButton = styled(Button)`
color: black;
font-size: 1rem;
font-weight: 600;
margin-top: ${btnSpacing};
line-height: 1.5;
}
`;
@ -115,6 +118,7 @@ const Title = styled.h2`
font-size: 1.3rem;
color: ${colorGrayDark};
white-space: normal;
margin: 0;
@media ${smallOnly} {
font-size: 1rem;

View File

@ -2,8 +2,8 @@ import React, { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import BBBMenu from "/imports/ui/components/common/menu/component";
import Button from '/imports/ui/components/common/button/component';
import CreateBreakoutRoomModal from '/imports/ui/components/actions-bar/create-breakout-room/container';
import Trigger from "/imports/ui/components/common/control-header/right/component";
const intlMessages = defineMessages({
options: {
@ -86,20 +86,16 @@ class BreakoutDropdown extends PureComponent {
render() {
const {
intl,
isRTL,
} = this.props;
return (
<>
<BBBMenu
trigger={
<Button
<Trigger
data-test="breakoutOptionsMenu"
icon="more"
size="sm"
ghost
circle
hideLabel
color="dark"
label={intl.formatMessage(intlMessages.options)}
aria-label={intl.formatMessage(intlMessages.options)}
onClick={() => null}
@ -112,8 +108,8 @@ class BreakoutDropdown extends PureComponent {
elevation: 3,
getContentAnchorEl: null,
fullwidth: "true",
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
transformorigin: { vertical: 'bottom', horizontal: 'left' },
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
}}
actions={this.getAvailableActions()}
/>

View File

@ -15,6 +15,7 @@ import Settings from '/imports/ui/services/settings';
import BreakoutDropdown from '/imports/ui/components/breakout-room/breakout-dropdown/component';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import Header from '/imports/ui/components/common/control-header/component';
const intlMessages = defineMessages({
breakoutTitle: {
@ -549,19 +550,19 @@ class BreakoutRoom extends PureComponent {
intl,
endAllBreakouts,
amIModerator,
isRTL,
} = this.props;
return (
<Styled.Panel ref={(n) => this.panel = n}>
<Styled.Header>
<Styled.HeaderButton
icon="left_arrow"
label={intl.formatMessage(intlMessages.breakoutTitle)}
aria-label={intl.formatMessage(intlMessages.breakoutAriaTitle)}
onClick={() => {
<Header
leftButtonProps={{
'aria-label': intl.formatMessage(intlMessages.breakoutAriaTitle),
label: intl.formatMessage(intlMessages.breakoutTitle),
onClick: () => {
this.closePanel();
}}
/>
{ amIModerator && (
},
}}
customRightButton={amIModerator && (
<BreakoutDropdown
openBreakoutTimeManager={this.showSetTimeForm}
endAllBreakouts={() => {
@ -570,9 +571,10 @@ class BreakoutRoom extends PureComponent {
}}
isMeteorConnected={isMeteorConnected}
amIModerator={amIModerator}
isRTL={isRTL}
/>
) }
</Styled.Header>
)}
/>
{this.renderDuration()}
{amIModerator
? (

View File

@ -4,7 +4,7 @@ import AudioService from '/imports/ui/components/audio/service';
import AudioManager from '/imports/ui/services/audio-manager';
import BreakoutComponent from './component';
import Service from './service';
import { layoutDispatch } from '../layout/context';
import { layoutDispatch, layoutSelect } from '../layout/context';
import Auth from '/imports/ui/services/auth';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import {
@ -18,10 +18,11 @@ const BreakoutContainer = (props) => {
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;
const amIPresenter = users[Auth.meetingID][Auth.userID].presenter;
const isRTL = layoutSelect((i) => i.isRTL);
return <BreakoutComponent
amIPresenter={amIPresenter}
{...{ layoutContextDispatch, ...props }}
{...{ layoutContextDispatch, isRTL, ...props }}
/>;
};

View File

@ -3,6 +3,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import deviceInfo from '/imports/utils/deviceInfo';
import PropTypes from 'prop-types';
import Styled from './styles';
import { escapeHtml } from '/imports/utils/string-utils';
import { isChatEnabled } from '/imports/ui/services/features';
const propTypes = {
@ -140,7 +141,7 @@ class MessageForm extends PureComponent {
handleSendMessage,
} = this.props;
const { message } = this.state;
let msg = message.trim();
const msg = message.trim();
if (msg.length < minMessageLength) return;
@ -150,13 +151,7 @@ class MessageForm extends PureComponent {
return;
}
// Sanitize. See: http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/
const div = document.createElement('div');
div.appendChild(document.createTextNode(msg));
msg = div.innerHTML;
handleSendMessage(msg);
handleSendMessage(escapeHtml(msg));
this.setState({ message: '', hasErrors: false });
}

View File

@ -10,7 +10,6 @@ import {
colorPrimary,
colorGray,
colorDanger,
colorGrayDark,
userListBg,
colorWhite,
colorGrayLighter,
@ -228,33 +227,6 @@ const Panel = styled(ScrollboxVertical)`
height: 100%;
`;
const HeaderButton = styled(Button)`
display: flex;
flex-direction: row;
justify-content: space-between;
position: relative;
padding-left: 0;
padding-right: inherit;
background: none !important;
[dir="rtl"] & {
margin: 0 0 2rem auto;
padding-left: inherit;
padding-right: 0;
}
& > i {
color: ${colorGrayDark};
[dir="rtl"] & {
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}`;
const Separator = styled.div`
position: relative;
width: 100%;
@ -264,13 +236,6 @@ const Separator = styled.div`
margin: 30px 0px;
`;
const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: ${jumboPaddingY};
`;
const FlexRow = styled.div`
display: flex;
flex-wrap: nowrap;
@ -296,8 +261,6 @@ export default {
EndButton,
Duration,
Panel,
HeaderButton,
Separator,
Header,
FlexRow,
};

View File

@ -8,6 +8,7 @@ import Service from '/imports/ui/components/captions/service';
import Styled from './styles';
import { PANELS, ACTIONS } from '/imports/ui/components/layout/enums';
import browserInfo from '/imports/utils/browserInfo';
import Header from '/imports/ui/components/common/control-header/component';
const intlMessages = defineMessages({
hide: {
@ -71,54 +72,50 @@ const Captions = ({
return (
<Styled.Captions isChrome={isChrome}>
<Styled.Header>
<Styled.Title>
<Styled.HideBtn
onClick={() => {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
}}
aria-label={intl.formatMessage(intlMessages.hide)}
label={name}
icon={isRTL ? 'right_arrow' : 'left_arrow'}
/>
</Styled.Title>
{Service.amICaptionsOwner(ownerId)
? (
<span>
<Button
onClick={dictating
? () => Service.stopDictation(locale)
: () => Service.startDictation(locale)}
label={dictating
? intl.formatMessage(intlMessages.dictationStop)
: intl.formatMessage(intlMessages.dictationStart)}
aria-describedby="dictationBtnDesc"
color={dictating ? 'danger' : 'primary'}
disabled={!dictation}
/>
<div id="dictationBtnDesc" hidden>
{dictating
? intl.formatMessage(intlMessages.dictationOffDesc)
: intl.formatMessage(intlMessages.dictationOnDesc)}
</div>
</span>
) : (
<Header
leftButtonProps={{
onClick: () => {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
},
'aria-label': intl.formatMessage(intlMessages.hide),
label: name,
}}
customRightButton={Service.amICaptionsOwner(ownerId) ? (
<span>
<Button
color="primary"
tooltipLabel={intl.formatMessage(intlMessages.takeOwnershipTooltip, { 0: name })}
onClick={() => Service.updateCaptionsOwner(locale, name)}
aria-label={intl.formatMessage(intlMessages.takeOwnership)}
label={intl.formatMessage(intlMessages.takeOwnership)}
onClick={dictating
? () => Service.stopDictation(locale)
: () => Service.startDictation(locale)}
label={dictating
? intl.formatMessage(intlMessages.dictationStop)
: intl.formatMessage(intlMessages.dictationStart)}
aria-describedby="dictationBtnDesc"
color={dictating ? 'danger' : 'primary'}
disabled={!dictation}
/>
)}
</Styled.Header>
<div id="dictationBtnDesc" hidden>
{dictating
? intl.formatMessage(intlMessages.dictationOffDesc)
: intl.formatMessage(intlMessages.dictationOnDesc)}
</div>
</span>
) : (
<Button
color="primary"
tooltipLabel={intl.formatMessage(intlMessages.takeOwnershipTooltip, { 0: name })}
onClick={() => Service.updateCaptionsOwner(locale, name)}
aria-label={intl.formatMessage(intlMessages.takeOwnership)}
label={intl.formatMessage(intlMessages.takeOwnership)}
/>
)}
/>
<PadContainer
externalId={locale}
hasPermission={hasPermission}

View File

@ -1,23 +1,14 @@
import styled from 'styled-components';
import {
colorWhite,
colorGrayDark,
} from '/imports/ui/stylesheets/styled-components/palette';
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import {
mdPaddingX,
mdPaddingY,
pollHeaderOffset,
toastContentWidth,
borderSize,
borderSizeLarge,
} from '/imports/ui/stylesheets/styled-components/general';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import { DivElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
import Button from '/imports/ui/components/common/button/component';
const Captions = styled.div`
background-color: ${colorWhite};
padding: ${mdPaddingX} ${mdPaddingY} ${mdPaddingX} ${mdPaddingX};
padding: ${mdPaddingY} ${mdPaddingY} ${mdPaddingX} ${mdPaddingX};
display: flex;
flex-grow: 1;
flex-direction: column;
@ -33,58 +24,4 @@ const Captions = styled.div`
}
`;
const Header = styled.header`
position: relative;
top: ${pollHeaderOffset};
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
`;
const Title = styled(DivElipsis)`
flex: 1;
& > button, & > button:hover {
max-width: ${toastContentWidth};
}
`;
const HideBtn = styled(Button)`
position: relative;
background-color: ${colorWhite};
display: block;
margin: ${borderSizeLarge};
margin-bottom: ${borderSize};
padding-left: 0;
padding-right: inherit;
[dir="rtl"] & {
padding-left: inherit;
padding-right: 0;
}
& > i {
color: ${colorGrayDark};
font-size: smaller;
[dir="rtl"] & {
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}
&:hover {
background-color: ${colorWhite};
}
`;
export default {
Captions,
Header,
Title,
HideBtn,
};
export default { Captions };

View File

@ -6,6 +6,7 @@ import _ from 'lodash';
import injectNotify from '/imports/ui/components/common/toast/inject-notify/component';
import AudioService from '/imports/ui/components/audio/service';
import ChatPushAlert from './push-alert/component';
import { stripTags, unescapeHtml } from '/imports/utils/string-utils';
import Service from '../service';
import Styled from './styles';
@ -146,11 +147,8 @@ const ChatAlert = (props) => {
if (content.text === PUBLIC_CHAT_CLEAR) {
return intl.formatMessage(intlMessages.publicChatClear);
}
/* this code is to remove html tags that come in the server's messages */
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content.text;
const textWithoutTag = tempDiv.innerText;
return textWithoutTag;
return unescapeHtml(stripTags(content.text));
});
return contentMessage;

View File

@ -2,12 +2,12 @@ import React, { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import _ from 'lodash';
import BBBMenu from '/imports/ui/components/common/menu/component';
import Button from '/imports/ui/components/common/button/component';
import { alertScreenReader } from '/imports/utils/dom-utils';
import BBBMenu from "/imports/ui/components/common/menu/component";
import { getDateString } from '/imports/utils/string-utils';
import Trigger from "/imports/ui/components/common/control-header/right/component";
import ChatService from '../service';
import { addNewAlert } from '../../screenreader-alert/service';
const intlMessages = defineMessages({
clear: {
@ -58,7 +58,6 @@ class ChatDropdown extends PureComponent {
meetingIsBreakout,
meetingName,
timeWindowsValues,
users,
} = this.props;
const clearIcon = 'delete';
@ -77,14 +76,11 @@ class ChatDropdown extends PureComponent {
onClick: () => {
const link = document.createElement('a');
const mimeType = 'text/plain';
const date = new Date();
const time = `${date.getHours()}-${date.getMinutes()}`;
const dateString = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${time}`;
link.setAttribute('download', `bbb-${meetingName}[public-chat]_${dateString}.txt`);
link.setAttribute('download', `bbb-${meetingName}[public-chat]_${getDateString()}.txt`);
link.setAttribute(
'href',
`data: ${mimeType} ;charset=utf-8,`
+ `${encodeURIComponent(ChatService.exportChat(timeWindowsValues, users, intl))}`,
`data: ${mimeType};charset=utf-8,`
+ `${encodeURIComponent(ChatService.exportChat(timeWindowsValues, intl))}`,
);
link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
},
@ -101,11 +97,11 @@ class ChatDropdown extends PureComponent {
dataTest: 'chatCopy',
label: intl.formatMessage(intlMessages.copy),
onClick: () => {
const chatHistory = ChatService.exportChat(timeWindowsValues, users, intl);
const chatHistory = ChatService.exportChat(timeWindowsValues, intl);
navigator.clipboard.writeText(chatHistory).then(() => {
alertScreenReader(intl.formatMessage(intlMessages.copySuccess));
addNewAlert(intl.formatMessage(intlMessages.copySuccess));
}).catch(() => {
alertScreenReader(intl.formatMessage(intlMessages.copyErr));
addNewAlert(intl.formatMessage(intlMessages.copyErr));
});
},
},
@ -131,26 +127,22 @@ class ChatDropdown extends PureComponent {
const {
intl,
amIModerator,
isRTL,
} = this.props;
if (!amIModerator && !ENABLE_SAVE_AND_COPY_PUBLIC_CHAT) return null;
return (
<>
<BBBMenu
trigger={(
<Button
trigger={
<Trigger
data-test="chatOptionsMenu"
icon="more"
size="sm"
ghost
circle
hideLabel
color="dark"
label={intl.formatMessage(intlMessages.options)}
aria-label={intl.formatMessage(intlMessages.options)}
onClick={() => null}
/>
)}
/>
}
opts={{
id: 'default-dropdown-menu',
keepMounted: true,
@ -158,8 +150,8 @@ class ChatDropdown extends PureComponent {
elevation: 3,
getContentAnchorEl: null,
fullwidth: 'true',
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
transformorigin: { vertical: 'bottom', horizontal: 'left' },
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
}}
actions={this.getAvailableActions()}
/>

View File

@ -1,15 +1,14 @@
import React, { useContext } from 'react';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/api/meetings';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import ChatDropdown from './component';
import { layoutSelect } from '../../layout/context';
const ChatDropdownContainer = ({ ...props }) => {
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;
const isRTL = layoutSelect((i) => i.isRTL);
return <ChatDropdown {...props} users={users[Auth.meetingID]} />;
return <ChatDropdown {...{ isRTL, ...props }} />;
};
export default withTracker(() => {

View File

@ -2,7 +2,6 @@ import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import Button from '/imports/ui/components/common/button/component';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import { Meteor } from 'meteor/meteor';
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
@ -14,6 +13,7 @@ import { PANELS, ACTIONS } from '../layout/enums';
import { UserSentMessageCollection } from './service';
import Auth from '/imports/ui/services/auth';
import browserInfo from '/imports/utils/browserInfo';
import Header from '/imports/ui/components/common/control-header/component';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
@ -67,68 +67,56 @@ const Chat = (props) => {
isChrome={isChrome}
data-test={isPublicChat ? 'publicChat' : 'privateChat'}
>
<Styled.Header>
<Styled.Title data-test="chatTitle">
<Styled.HideChatButton
onClick={() => {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_ID_CHAT_OPEN,
value: '',
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
}}
aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })}
accessKey={chatID !== 'public' ? HIDE_CHAT_AK : null}
data-test={isPublicChat ? 'hidePublicChat' : 'hidePrivateChat'}
label={title}
icon="left_arrow"
<Header
leftButtonProps={{
accessKey: chatID !== 'public' ? HIDE_CHAT_AK : null,
'aria-label': intl.formatMessage(intlMessages.hideChatLabel, { 0: title }),
'data-test': isPublicChat ? 'hidePublicChat' : 'hidePrivateChat',
label: title,
onClick: () => {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_ID_CHAT_OPEN,
value: '',
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
},
}}
rightButtonProps={{
accessKey: CLOSE_CHAT_AK,
'aria-label': intl.formatMessage(intlMessages.closeChatLabel, { 0: title }),
'data-test': "closePrivateChat",
icon: "close",
label: intl.formatMessage(intlMessages.closeChatLabel, { 0: title }),
onClick: () => {
actions.handleClosePrivateChat(chatID);
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_ID_CHAT_OPEN,
value: '',
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
},
}}
customRightButton={isPublicChat && (
<ChatDropdownContainer {...{
meetingIsBreakout, isMeteorConnected, amIModerator, timeWindowsValues,
}}
/>
</Styled.Title>
{
!isPublicChat
? (
<Button
icon="close"
size="sm"
ghost
color="dark"
hideLabel
onClick={() => {
actions.handleClosePrivateChat(chatID);
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_ID_CHAT_OPEN,
value: '',
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
}}
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
accessKey={CLOSE_CHAT_AK}
data-test="closePrivateChat"
/>
)
: (
<ChatDropdownContainer {...{
meetingIsBreakout, isMeteorConnected, amIModerator, timeWindowsValues,
}}
/>
)
}
</Styled.Header>
)}
/>
<TimeWindowList
id={ELEMENT_ID}
chatId={chatID}

View File

@ -13,6 +13,7 @@ import lockContextContainer from '/imports/ui/components/lock-viewers/context/co
import Chat from '/imports/ui/components/chat/component';
import ChatService from './service';
import { layoutSelect, layoutDispatch } from '../layout/context';
import { escapeHtml } from '/imports/utils/string-utils';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
@ -200,7 +201,7 @@ const ChatContainer = (props) => {
id,
content: [{
id,
text: intl.formatMessage(intlMessages.partnerDisconnected, { 0: chatName }),
text: escapeHtml(intl.formatMessage(intlMessages.partnerDisconnected, { 0: chatName })),
time,
}],
time,

View File

@ -7,6 +7,7 @@ import _ from 'lodash';
import TypingIndicatorContainer from './typing-indicator/container';
import ClickOutside from '/imports/ui/components/click-outside/component';
import Styled from './styles';
import { escapeHtml } from '/imports/utils/string-utils';
import { isChatEnabled } from '/imports/ui/services/features';
const propTypes = {
@ -257,15 +258,9 @@ class MessageForm extends PureComponent {
return;
}
// Sanitize. See: http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/
const div = document.createElement('div');
div.appendChild(document.createTextNode(msg));
msg = div.innerHTML;
const callback = this.typingIndicator ? stopUserTyping : null;
handleSendMessage(msg);
handleSendMessage(escapeHtml(msg));
this.setState({ message: '', hasErrors: false, showEmojiPicker: false }, callback);
}

View File

@ -6,6 +6,7 @@ import UnreadMessages from '/imports/ui/services/unread-messages';
import Storage from '/imports/ui/services/storage/session';
import { makeCall } from '/imports/ui/services/api';
import _ from 'lodash';
import { stripTags, unescapeHtml } from '/imports/utils/string-utils';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
import { defineMessages } from 'react-intl';
import PollService from '/imports/ui/components/poll/service';
@ -246,17 +247,13 @@ const removeFromClosedChatsSession = (idChatOpen) => {
}
};
// We decode to prevent HTML5 escaped characters.
const htmlDecode = (input) => {
const e = document.createElement('div');
e.innerHTML = input;
const messages = Array.from(e.childNodes);
const message = messages.map((chatMessage) => chatMessage.textContent);
return message.join('');
const replacedBRs = input.replaceAll('<br/>', '\n');
return unescapeHtml(stripTags(replacedBRs));
};
// Export the chat as [Hour:Min] user: message
const exportChat = (timeWindowList, users, intl) => {
const exportChat = (timeWindowList, intl) => {
const messageList = timeWindowList.reduce((acc, timeWindow) => {
const msgs = timeWindow.content.map((message) => {
const date = new Date(message.time);
@ -270,7 +267,7 @@ const exportChat = (timeWindowList, users, intl) => {
let userName = message.id.startsWith(SYSTEM_CHAT_TYPE)
? ''
: `${users[timeWindow.sender].name}: `;
: `${timeWindow.senderName}: `;
let messageText = '';
if (message.text === PUBLIC_CHAT_CLEAR) {
message.text = intl.formatMessage(intlMessages.publicChatClear);

View File

@ -1,23 +1,14 @@
import styled from 'styled-components';
import {
colorWhite,
colorGrayDark,
colorPrimary
} from '/imports/ui/stylesheets/styled-components/palette';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import Button from '/imports/ui/components/common/button/component';
import {
mdPaddingX,
mdPaddingY,
pollHeaderOffset,
borderSizeLarge,
borderSize,
} from '/imports/ui/stylesheets/styled-components/general';
import { DivElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
import { mdPaddingX } from '/imports/ui/stylesheets/styled-components/general';
const Chat = styled.div`
background-color: ${colorWhite};
padding: ${mdPaddingX} ${mdPaddingY} ${mdPaddingX} ${mdPaddingX};
padding: ${mdPaddingX};
display: flex;
flex-grow: 1;
@ -64,59 +55,4 @@ const Chat = styled.div`
}
`;
const Header = styled.header`
position: relative;
top: ${pollHeaderOffset};
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
`;
const Title = styled(DivElipsis)`
flex: 1;
& > button, button:hover {
max-width: 98%;
}
`;
const HideChatButton = styled(Button)`
position: relative;
background-color: ${colorWhite};
display: block;
margin: ${borderSizeLarge};
margin-bottom: ${borderSize};
padding-left: 0;
padding-right: inherit;
z-index: 3;
[dir="rtl"] & {
padding-left: inherit;
padding-right: 0;
}
& > i {
color: ${colorGrayDark};
font-size: smaller;
[dir="rtl"] & {
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}
&:hover {
background-color: ${colorWhite};
}
`;
export default {
Chat,
Header,
Title,
HideChatButton,
};
export default { Chat };

View File

@ -45,6 +45,11 @@ const EmojiButton = styled.button`
z-index: 2;
border: none;
[dir="rtl"] & {
right: initial;
left: -.2em;
}
&:hover {
transform: scale(1.5);
transition-duration: 150ms;
@ -60,6 +65,15 @@ const EmojiButton = styled.button`
margin-top: 40%;
color: ${btnDefaultColor};
}
${({ rotate }) => rotate && `
span {
i {
transform: rotate(180deg);
margin-top: 20%;
}
}
`}
`;
const EmojiButtonSpace = styled.div`
@ -70,6 +84,11 @@ const EmojiButtonSpace = styled.div`
right: -.4em;
bottom: -.2em;
border-radius: 50%;
[dir="rtl"] & {
right: initial;
left: -.4em;
}
`;
export default {

View File

@ -95,7 +95,8 @@ const ButtonLabel = styled.span`
margin: 0 ${btnSpacing} 0 0;
}
}
&:hover {
&:hover,
.buttonWrapper:hover & {
opacity: .5;
}
@ -355,13 +356,13 @@ const ButtonSpan = styled.span`
padding: ${jumboPaddingY} ${jumboPaddingX};
`}
${({ color }) => color === 'default' && `
${({ color, ghost }) => color === 'default' && !ghost && `
color: ${btnDefaultColor};
background-color: ${btnDefaultBg};
border: ${borderSizeLarge} solid transparent;
&:focus,
&:focus:not([aria-disabled="true"]) & {
.buttonWrapper:focus:not([aria-disabled="true"]) & {
color: ${btnDefaultColor};
background-color: ${btnDefaultBg};
background-clip: padding-box;
@ -515,7 +516,7 @@ const ButtonSpan = styled.span`
color: ${btnDefaultBg};
background-color: ${btnDefaultColor};
background-clip: padding-box;
box-shadow: 0 0 0 ${borderSizeLarge} ${btnDefaultBg};
box-shadow: 0 0 0 ${borderSizeLarge} ${btnDefaultBg} !important;
}
&:hover,
.buttonWrapper:hover & {

View File

@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import Styled from './styles';
import Left from './left/component';
import Right from './right/component';
const Header = ({
leftButtonProps,
rightButtonProps,
customRightButton,
}) => {
const renderCloseButton = () => (
<Right {...rightButtonProps} />
);
const renderCustomRightButton = () => (
<Styled.RightWrapper>
{customRightButton}
</Styled.RightWrapper>
);
return (
<Styled.Header>
<Left {...leftButtonProps} />
{customRightButton
? renderCustomRightButton()
: rightButtonProps
? renderCloseButton()
: null}
</Styled.Header>
);
}
Header.propTypes = {
leftButtonProps: PropTypes.object,
rightButtonProps: PropTypes.object,
customRightButton: PropTypes.element,
};
export default Header;

View File

@ -0,0 +1,30 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Styled from './styles';
class Left extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Styled.HideButton
className="buttonWrapper"
icon="left_arrow"
tabindex={0}
{...this.props}
/>
);
}
}
Left.propTypes = {
accessKey: PropTypes.any,
'aria-label': PropTypes.string,
'data-test': PropTypes.string,
label: PropTypes.string,
onClick: PropTypes.func,
};
export default Left;

View File

@ -0,0 +1,31 @@
import styled from 'styled-components';
import Button from '/imports/ui/components/common/button/component';
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
const HideButton = styled(Button)`
padding: 0;
margin: 0;
line-height: normal;
display: flex;
align-items: center;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
& > i,
& > i::before {
width: auto;
font-size: ${fontSizeBase} !important;
[dir="rtl"] & {
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}
`;
export default { HideButton };

View File

@ -0,0 +1,31 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Styled from './styles';
class Right extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Styled.CloseButton
size="sm"
hideLabel
circle
{...this.props}
/>
);
}
}
Right.propTypes = {
accessKey: PropTypes.any,
'aria-label': PropTypes.string,
'data-test': PropTypes.string,
icon: PropTypes.string,
label: PropTypes.string,
onClick: PropTypes.func,
};
export default Right;

View File

@ -0,0 +1,26 @@
import styled from 'styled-components';
import Button from '/imports/ui/components/common/button/component';
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
const CloseButton = styled(Button)`
span:first-of-type {
padding: 0;
margin: 0;
& > i,
& > i::before {
width: auto;
font-size: ${fontSizeBase};
}
&:hover,
&:focus,
.buttonWrapper:hover &,
.buttonWrapper:focus & {
background-color: ${colorWhite} !important;
}
}
`;
export default { CloseButton };

View File

@ -0,0 +1,20 @@
import styled from 'styled-components';
import { jumboPaddingY } from '/imports/ui/stylesheets/styled-components/general';
const Header = styled.header`
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: ${jumboPaddingY};
`;
const RightWrapper = styled.div`
& > div {
display: flex;
}
`;
export default {
Header,
RightWrapper,
};

View File

@ -26,7 +26,7 @@ class BBBMenu extends React.Component {
anchorEl: null,
};
this.opts = props.opts;
this.optsToMerge = {};
this.autoFocus = false;
this.handleClick = this.handleClick.bind(this);
@ -70,12 +70,12 @@ class BBBMenu extends React.Component {
const emojiSelected = key?.toLowerCase()?.includes(selectedEmoji?.toLowerCase());
let customStyles = {
paddingLeft: '4px',
paddingRight: '4px',
paddingTop: '8px',
paddingBottom: '8px',
marginLeft: '4px',
marginRight: '4px'
paddingLeft: '16px',
paddingRight: '16px',
paddingTop: '12px',
paddingBottom: '12px',
marginLeft: '0px',
marginRight: '0px',
};
if (a.customStyles) {
@ -99,11 +99,11 @@ class BBBMenu extends React.Component {
if (close) this.handleClose(event);
event.stopPropagation();
}}>
<div style={{ display: 'flex', flexFlow: 'row', width: '100%' }}>
<Styled.MenuItemWrapper>
{a.icon ? <Icon iconName={a.icon} key="icon" /> : null}
<Styled.Option>{label}</Styled.Option>
{a.iconRight ? <Styled.IconRight iconName={a.iconRight} key="iconRight" /> : null}
</div>
</Styled.MenuItemWrapper>
</Styled.BBBMenuItem>,
a.divider && <Divider disabled />
];
@ -112,7 +112,7 @@ class BBBMenu extends React.Component {
render() {
const { anchorEl } = this.state;
const { trigger, intl, customStyles, dataTest } = this.props;
const { trigger, intl, customStyles, dataTest, opts } = this.props;
const actionsItems = this.makeMenuItems();
let menuStyles = { zIndex: 9999 };
@ -129,7 +129,7 @@ class BBBMenu extends React.Component {
const firefoxInputSource = !([1, 5].includes(e.nativeEvent.mozInputSource)); // 1 = mouse, 5 = touch (firefox only)
const chromeInputSource = !(['mouse', 'touch'].includes(e.nativeEvent.pointerType));
this.opts.autoFocus = firefoxInputSource && chromeInputSource;
this.optsToMerge.autoFocus = firefoxInputSource && chromeInputSource;
this.handleClick(e);
}}
onKeyPress={(e) => {
@ -144,7 +144,8 @@ class BBBMenu extends React.Component {
</div>
<Menu
{...this.opts}
{...opts}
{...this.optsToMerge}
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={this.handleClose}

View File

@ -6,6 +6,13 @@ import { colorWhite, colorPrimary } from '/imports/ui/stylesheets/styled-compone
import { fontSizeLarge } from '/imports/ui/stylesheets/styled-components/typography';
import { mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
const MenuItemWrapper = styled.div`
display: flex;
flex-flow: row;
width: 100%;
align-items: center;
`;
const Option = styled.div`
line-height: 1;
margin-right: 1.65rem;
@ -78,6 +85,7 @@ const BBBMenuItem = styled(MenuItem)`
`;
export default {
MenuItemWrapper,
Option,
CloseButton,
IconRight,

View File

@ -1,8 +1,10 @@
import { useState, useContext, useEffect } from 'react';
import {
useState, useContext, useRef, useEffect,
} from 'react';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import { throttle } from 'lodash';
const USER_JOIN_UPDATE_TIMEOUT = 1000;
let updateTimeout = null;
const USER_JOIN_UPDATE_THROTTLE_TIME = 1000;
export default function useContextUsers() {
const usingUsersContext = useContext(UsersContext);
@ -11,17 +13,15 @@ export default function useContextUsers() {
const [users, setUsers] = useState(null);
const [isReady, setIsReady] = useState(true);
const throttledSetUsers = useRef(throttle(() => {
setUsers(contextUsers);
setIsReady(true);
},
USER_JOIN_UPDATE_THROTTLE_TIME, { trailing: true }));
useEffect(() => {
setIsReady(false);
if (updateTimeout) {
clearTimeout(updateTimeout);
}
updateTimeout = setTimeout(() => {
setUsers(contextUsers);
setIsReady(true);
}, USER_JOIN_UPDATE_TIMEOUT);
throttledSetUsers.current();
}, [contextUsers]);
return {

View File

@ -162,6 +162,7 @@ class ConnectionStatusComponent extends PureComponent {
dataPage: '1',
dataSaving: props.dataSaving,
hasNetworkData: false,
copyButtonText: intl.formatMessage(intlMessages.copy),
networkData: {
user: {
@ -180,6 +181,7 @@ class ConnectionStatusComponent extends PureComponent {
},
};
this.displaySettingsStatus = this.displaySettingsStatus.bind(this);
this.setButtonMessage = this.setButtonMessage.bind(this);
this.rateInterval = null;
this.audioUploadLabel = intl.formatMessage(intlMessages.audioUploadRate);
this.audioDownloadLabel = intl.formatMessage(intlMessages.audioDownloadRate);
@ -287,6 +289,12 @@ class ConnectionStatusComponent extends PureComponent {
);
}
setButtonMessage(msg) {
this.setState({
copyButtonText: msg,
});
}
/**
* Copy network data to clipboard
* @param {Object} e Event object from click event
@ -305,14 +313,14 @@ class ConnectionStatusComponent extends PureComponent {
const { target: copyButton } = e;
copyButton.innerHTML = intl.formatMessage(intlMessages.copied);
this.setButtonMessage(intl.formatMessage(intlMessages.copied));
const data = JSON.stringify(networkData, null, 2);
await navigator.clipboard.writeText(data);
this.copyNetworkDataTimeout = setTimeout(() => {
copyButton.innerHTML = intl.formatMessage(intlMessages.copy);
this.setButtonMessage(intl.formatMessage(intlMessages.copy));
}, MIN_TIMEOUT);
}
@ -623,7 +631,7 @@ class ConnectionStatusComponent extends PureComponent {
onKeyPress={this.copyNetworkData.bind(this)}
tabIndex={0}
>
{intl.formatMessage(intlMessages.copy)}
{this.state.copyButtonText}
</Styled.Copy>
</Styled.CopyContainer>
);

View File

@ -40,6 +40,9 @@ const intlMessages = defineMessages({
banned_user_rejoining_reason: {
id: 'app.error.userBanned',
},
joined_another_window_reason: {
id: 'app.error.joinedAnotherWindow',
},
});
const propTypes = {

View File

@ -6,8 +6,8 @@ export const INITIAL_INPUT_STATE = {
},
browser: {
width: 0,
height: 0,
width: window.document.documentElement.clientWidth,
height: window.document.documentElement.clientHeight,
},
bannerBar: {
hasBanner: false,

View File

@ -8,11 +8,11 @@ import RecordingIndicator from './recording-indicator/container';
import TalkingIndicatorContainer from '/imports/ui/components/nav-bar/talking-indicator/container';
import ConnectionStatusButton from '/imports/ui/components/connection-status/button/container';
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
import { addNewAlert } from '/imports/ui/components/screenreader-alert/service';
import SettingsDropdownContainer from './settings-dropdown/container';
import browserInfo from '/imports/utils/browserInfo';
import deviceInfo from '/imports/utils/deviceInfo';
import _ from "lodash";
import { politeSRAlert } from '/imports/utils/dom-utils';
import { PANELS, ACTIONS } from '../layout/enums';
const intlMessages = defineMessages({
@ -182,7 +182,7 @@ class NavBar extends Component {
activeChats.map((c, i) => {
if (c?.unreadCounter > 0 && c?.unreadCounter !== acs[i]?.unreadCounter) {
politeSRAlert(`${intl.formatMessage(intlMessages.newMsgAria, { 0: c.name })}`)
addNewAlert(`${intl.formatMessage(intlMessages.newMsgAria, { 0: c.name })}`);
}
});

View File

@ -279,9 +279,10 @@ class SettingsDropdown extends PureComponent {
shortcuts: OPEN_OPTIONS_AK,
isDropdownOpen,
isMobile,
isRTL,
} = this.props;
const customStyles = { top: '3rem' };
const customStyles = { top: '1rem' };
return (
<BBBMenu
@ -302,6 +303,16 @@ class SettingsDropdown extends PureComponent {
/>
)}
actions={this.renderMenuItems()}
opts={{
id: "default-dropdown-menu",
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getContentAnchorEl: null,
fullwidth: "true",
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'left' : 'right' },
transformorigin: { vertical: 'top', horizontal: isRTL ? 'left' : 'right' },
}}
/>
);
}

View File

@ -5,7 +5,7 @@ import browserInfo from '/imports/utils/browserInfo';
import SettingsDropdown from './component';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
import { layoutSelectInput } from '../../layout/context';
import { layoutSelectInput, layoutSelect } from '../../layout/context';
import { SMALL_VIEWPORT_BREAKPOINT } from '../../layout/enums';
const { isIphone } = deviceInfo;
@ -16,9 +16,10 @@ const noIOSFullscreen = !!(((isSafari && !isValidSafariVersion) || isIphone));
const SettingsDropdownContainer = (props) => {
const { width: browserWidth } = layoutSelectInput((i) => i.browser);
const isMobile = browserWidth <= SMALL_VIEWPORT_BREAKPOINT;
const isRTL = layoutSelect((i) => i.isRTL);
return (
<SettingsDropdown {...{ isMobile, ...props }} />
<SettingsDropdown {...{ isMobile, isRTL, ...props }} />
);
};

View File

@ -8,6 +8,7 @@ import ConverterButtonContainer from './converter-button/container';
import Styled from './styles';
import { PANELS, ACTIONS } from '../layout/enums';
import browserInfo from '/imports/utils/browserInfo';
import Header from '/imports/ui/components/common/control-header/component';
const intlMessages = defineMessages({
hide: {
@ -42,27 +43,26 @@ const Notes = ({
return (
<Styled.Notes data-test="notes" isChrome={isChrome}>
<Styled.Header>
<Styled.Title data-test="notesTitle">
<Styled.HideButton
onClick={() => {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
}}
data-test="hideNotesLabel"
aria-label={intl.formatMessage(intlMessages.hide)}
label={intl.formatMessage(intlMessages.title)}
icon={isRTL ? 'right_arrow' : 'left_arrow'}
/>
</Styled.Title>
<ConverterButtonContainer />
</Styled.Header>
<Header
leftButtonProps={{
onClick: () => {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
},
'data-test': 'hideNotesLabel',
'aria-label': intl.formatMessage(intlMessages.hide),
label: intl.formatMessage(intlMessages.title),
}}
customRightButton={
<ConverterButtonContainer />
}
/>
<PadContainer
externalId={Service.ID}
hasPermission={hasPermission}

View File

@ -13,14 +13,7 @@ const ConvertAndUpload = styled(Button)`
position: relative;
background-color: ${colorWhite};
display: block;
margin: ${borderSizeLarge};
margin-bottom: ${borderSize};
padding-left: 0;
padding-right: inherit;
[dir="rtl"] & {
padding-left: inherit;
padding-right: 0;
}
padding: 0;
& > i {
color: ${colorGrayDark};
font-size: smaller;

View File

@ -1,23 +1,13 @@
import styled from 'styled-components';
import {
mdPaddingX,
mdPaddingY,
pollHeaderOffset,
toastContentWidth,
borderSizeLarge,
borderSize,
} from '/imports/ui/stylesheets/styled-components/general';
import {
colorWhite,
colorGrayDark,
} from '/imports/ui/stylesheets/styled-components/palette';
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import { DivElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
import Button from '/imports/ui/components/common/button/component';
const Notes = styled.div`
background-color: ${colorWhite};
padding: ${mdPaddingX} ${mdPaddingY} ${mdPaddingX} ${mdPaddingX};
padding: ${mdPaddingX};
display: flex;
flex-grow: 1;
flex-direction: column;
@ -33,58 +23,4 @@ const Notes = styled.div`
}
`;
const Header = styled.header`
position: relative;
top: ${pollHeaderOffset};
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
`;
const Title = styled(DivElipsis)`
flex: 1;
& > button, button:hover {
max-width: ${toastContentWidth};
}
`;
const HideButton = styled(Button)`
position: relative;
background-color: ${colorWhite};
display: block;
margin: ${borderSizeLarge};
margin-bottom: ${borderSize};
padding-left: 0;
padding-right: inherit;
[dir="rtl"] & {
padding-left: inherit;
padding-right: 0;
}
& > i {
color: ${colorGrayDark};
font-size: smaller;
[dir="rtl"] & {
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}
&:hover {
background-color: ${colorWhite};
}
`;
export default {
Notes,
Header,
Title,
HideButton,
};
export default { Notes };

View File

@ -1,6 +1,7 @@
import styled from 'styled-components';
import {
colorGray,
colorGrayLightest
} from '/imports/ui/stylesheets/styled-components/palette';
const Wrapper = styled.div`
@ -16,8 +17,7 @@ font-size: 15px;
color: ${colorGray};
bottom: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
display: block;
overflow-x: hidden;
overflow-wrap: break-word;
word-break: break-all;
@ -42,6 +42,8 @@ top: 0;
const Iframe = styled.iframe`
border-width: 0;
width: 100%;
border-top: 1px solid ${colorGrayLightest};
border-bottom: 1px solid ${colorGrayLightest};
`;
export default {

View File

@ -1,5 +1,5 @@
import _ from 'lodash';
import Pads, { PadsUpdates } from '/imports/api/pads';
import Pads, { PadsSessions, PadsUpdates } from '/imports/api/pads';
import { makeCall } from '/imports/ui/services/api';
import Auth from '/imports/ui/services/auth';
import Settings from '/imports/ui/services/settings';
@ -47,9 +47,13 @@ const throttledCreateSession = _.throttle(createSession, THROTTLE_TIMEOUT, {
const buildPadURL = (padId) => {
if (padId) {
const params = getParams();
const url = Auth.authenticateURL(`${PADS_CONFIG.url}/p/${padId}?${params}`);
return url;
const padsSessions = PadsSessions.findOne({});
if (padsSessions && padsSessions.sessions) {
const params = getParams();
const sessionIds = padsSessions.sessions.map(session => Object.values(session)).join(',');
const url = Auth.authenticateURL(`${PADS_CONFIG.url}/auth_session?padName=${padId}&sessionID=${sessionIds}&${params}`);
return url;
}
}
return null;

View File

@ -8,9 +8,10 @@ import DraggableTextArea from '/imports/ui/components/poll/dragAndDrop/component
import LiveResult from '/imports/ui/components/poll/live-result/component';
import Styled from './styles';
import Toggle from '/imports/ui/components/common/switch/component';
import { alertScreenReader } from '/imports/utils/dom-utils';
import { PANELS, ACTIONS } from '/imports/ui/components/layout/enums';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import { PANELS, ACTIONS } from '../layout/enums';
import { addNewAlert } from '../screenreader-alert/service';
import Header from '/imports/ui/components/common/control-header/component';
const intlMessages = defineMessages({
pollPaneTitle: {
@ -426,7 +427,7 @@ class Poll extends Component {
: [],
warning: clearWarning ? null : warning,
}, () => {
alertScreenReader(`${intl.formatMessage(intlMessages.removePollOpt,
addNewAlert(`${intl.formatMessage(intlMessages.removePollOpt,
{ 0: removed.val || intl.formatMessage(intlMessages.emptyPollOpt) })}`);
});
}
@ -977,15 +978,12 @@ class Poll extends Component {
return (
<div>
<Styled.Header>
<Styled.PollHideButton
ref={(node) => { this.hideBtn = node; }}
data-test="hidePollDesc"
tabIndex={0}
label={intl.formatMessage(intlMessages.pollPaneTitle)}
icon="left_arrow"
aria-label={intl.formatMessage(intlMessages.hidePollDesc)}
onClick={() => {
<Header
leftButtonProps={{
'aria-label': intl.formatMessage(intlMessages.hidePollDesc),
'data-test': "hidePollDesc",
label: intl.formatMessage(intlMessages.pollPaneTitle),
onClick: () => {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
@ -994,12 +992,15 @@ class Poll extends Component {
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
}}
/>
<Styled.PollCloseButton
label={intl.formatMessage(intlMessages.closeLabel)}
aria-label={`${intl.formatMessage(intlMessages.closeLabel)} ${intl.formatMessage(intlMessages.pollPaneTitle)}`}
onClick={() => {
},
ref: (node) => { this.hideBtn = node; },
}}
rightButtonProps={{
'aria-label': `${intl.formatMessage(intlMessages.closeLabel)} ${intl.formatMessage(intlMessages.pollPaneTitle)}`,
'data-test': "closePolling",
icon: "close",
label: intl.formatMessage(intlMessages.closeLabel),
onClick: () => {
if (currentPoll) stopPoll();
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
@ -1011,13 +1012,9 @@ class Poll extends Component {
});
Session.set('forcePollOpen', false);
Session.set('pollInitiated', false);
}}
icon="close"
size="sm"
hideLabel
data-test="closePolling"
/>
</Styled.Header>
},
}}
/>
{this.renderPollPanel()}
<span className="sr-only" id="poll-config-button">{intl.formatMessage(intlMessages.showRespDesc)}</span>
<span className="sr-only" id="add-item-button">{intl.formatMessage(intlMessages.addRespDesc)}</span>

View File

@ -1,5 +1,6 @@
import Auth from '/imports/ui/services/auth';
import { CurrentPoll } from '/imports/api/polls';
import { escapeHtml } from '/imports/utils/string-utils';
import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer';
import { defineMessages } from 'react-intl';
@ -113,12 +114,7 @@ const isDefaultPoll = (pollType) => pollType !== pollTypes.Custom
const getPollResultString = (pollResultData, intl) => {
const formatBoldBlack = (s) => s.bold().fontcolor('black');
// Sanitize. See: https://gist.github.com/sagewall/47164de600df05fb0f6f44d48a09c0bd
const sanitize = (value) => {
const div = document.createElement('div');
div.appendChild(document.createTextNode(value));
return div.innerHTML;
};
const sanitize = (value) => escapeHtml(value);
const { answers, numRespondents, questionType } = pollResultData;
const ísDefault = isDefaultPoll(questionType);

View File

@ -5,13 +5,10 @@ import {
smPaddingY,
lgPaddingX,
borderRadius,
mdPaddingY,
borderSize,
borderSizeLarge,
pollInputHeight,
pollSmMargin,
pollMdMargin,
pollHeaderOffset,
} from '/imports/ui/stylesheets/styled-components/general';
import {
colorText,
@ -338,56 +335,6 @@ const DragAndDropPollContainer = styled.div`
height: 200px !important;
`;
const Header = styled.header`
position: relative;
top: ${pollHeaderOffset};
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: ${mdPaddingY};
`;
const PollHideButton = styled(Button)`
position: relative;
background-color: ${colorWhite};
display: block;
margin: ${borderSizeLarge};
margin-bottom: ${borderSize};
padding-left: 0;
padding-right: inherit;
[dir="rtl"] & {
padding-left: inherit;
padding-right: 0;
}
> i {
color: ${colorGrayDark};
font-size: smaller;
[dir="rtl"] & {
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}
&:hover {
background-color: ${colorWhite};
}
`;
const PollCloseButton = styled(Button)`
font-size: ${fontSizeBase};
position: relative;
& > i {
color: ${colorText};
}
`;
const Question = styled.div`
margin-bottom: ${lgPaddingX};
`;
@ -424,9 +371,6 @@ export default {
NoSlidePanelContainer,
PollButton,
DragAndDropPollContainer,
Header,
PollHideButton,
PollCloseButton,
Warning,
CustomInputRow,
Question,

View File

@ -6,7 +6,6 @@ import WhiteboardToolbarContainer from '/imports/ui/components/whiteboard/whiteb
import { HUNDRED_PERCENT, MAX_PERCENT } from '/imports/utils/slideCalcUtils';
import { defineMessages, injectIntl } from 'react-intl';
import { toast } from 'react-toastify';
import { politeSRAlert } from '/imports/utils/dom-utils';
import { Session } from 'meteor/session';
import PresentationToolbarContainer from './presentation-toolbar/container';
import PresentationPlaceholder from './presentation-placeholder/component';
@ -25,6 +24,7 @@ import { ACTIONS, LAYOUT_TYPE } from '../layout/enums';
import DEFAULT_VALUES from '../layout/defaultValues';
import { colorContentBackground } from '/imports/ui/stylesheets/styled-components/palette';
import browserInfo from '/imports/utils/browserInfo';
import { addNewAlert } from '../screenreader-alert/service';
const intlMessages = defineMessages({
presentationLabel: {
@ -177,7 +177,7 @@ class Presentation extends PureComponent {
&& prevProps?.currentSlide?.num != null
&& currentSlide?.num !== prevProps.currentSlide?.num
) {
politeSRAlert(intl.formatMessage(intlMessages.slideContentChanged, { 0: currentSlide.num }));
addNewAlert(intl.formatMessage(intlMessages.slideContentChanged, { 0: currentSlide.num }));
}
if (currentPresentation) {

View File

@ -0,0 +1,5 @@
import { Meteor } from 'meteor/meteor';
const ScreenReaderAlertCollection = new Mongo.Collection('Screenreader-alert', { connection: null });
export default ScreenReaderAlertCollection;

View File

@ -0,0 +1,15 @@
import { createPortal } from 'react-dom';
import { useEffect } from 'react';
import { removeAlert } from './service';
const ARIA_ALERT_EXT_TIMEOUT = 15000;
const ScreenReaderAlert = ({ olderAlert }) => {
useEffect(() => {
if (olderAlert) setTimeout(() => removeAlert(olderAlert.id), ARIA_ALERT_EXT_TIMEOUT);
}, [olderAlert?.id]);
return olderAlert ? createPortal(olderAlert.text, document.getElementById('aria-polite-alert')) : null;
};
export default ScreenReaderAlert;

View File

@ -0,0 +1,19 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import ScreenReaderAlert from './component';
import ScreenReaderAlertCollection from './collection';
const ScreenReaderAlertContainer = ({ ...props }) => {
return (
<ScreenReaderAlert
{...{ ...props }}
/>
);
};
export default withTracker(() => {
const olderAlert = ScreenReaderAlertCollection
.findOne({}, { sort: { insertTime: +1 } });
return { olderAlert };
})(ScreenReaderAlertContainer);

View File

@ -0,0 +1,19 @@
import _ from 'lodash';
import ScreenReaderAlertCollection from './collection';
export const addNewAlert = (text) => {
const payload = {
id: _.uniqueId('alert-'),
insertedTime: Date.now(),
text,
};
return ScreenReaderAlertCollection.insert(payload);
};
export const removeAlert = (id) => ScreenReaderAlertCollection.remove({ id });
export default {
addNewAlert,
removeAlert,
};

View File

@ -18,6 +18,7 @@ import { Session } from 'meteor/session';
import Settings from '/imports/ui/services/settings';
import { notify } from '/imports/ui/services/notification';
import { FormattedMessage } from 'react-intl';
import { getDateString } from '/imports/utils/string-utils';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
@ -666,13 +667,10 @@ export const getUserNamesLink = (docTitle, fnSortedLabel, lnSortedLabel) => {
const link = document.createElement('a');
const meeting = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { 'meetingProp.name': 1 } });
const date = new Date();
const time = `${date.getHours()}-${date.getMinutes()}`;
const dateString = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${time}`;
link.setAttribute('download', `bbb-${meeting.meetingProp.name}[users-list]_${dateString}.txt`);
link.setAttribute('download', `bbb-${meeting.meetingProp.name}[users-list]_${getDateString()}.txt`);
link.setAttribute(
'href',
`data: ${mimeType} ;charset=utf-16,${encodeURIComponent(namesListsString)}`,
`data: ${mimeType};charset=utf-16,${encodeURIComponent(namesListsString)}`,
);
return link;
};

View File

@ -22,9 +22,9 @@ const UserParticipantsContainer = (props) => {
const { videoUsers, whiteboardUsers } = props;
const { users: contextUsers, isReady } = useContextUsers();
const currentUser = contextUsers ? contextUsers[Auth.meetingID][Auth.userID] : null;
const usersArray = contextUsers ? Object.values(contextUsers[Auth.meetingID]) : null;
const users = contextUsers ? formatUsers(usersArray, videoUsers, whiteboardUsers) : [];
const currentUser = contextUsers && isReady ? contextUsers[Auth.meetingID][Auth.userID] : null;
const usersArray = contextUsers && isReady ? Object.values(contextUsers[Auth.meetingID]) : null;
const users = contextUsers && isReady ? formatUsers(usersArray, videoUsers, whiteboardUsers) : [];
return (
<UserParticipants {

View File

@ -323,7 +323,7 @@ class UserOptions extends PureComponent {
}
render() {
const { intl } = this.props;
const { intl, isRTL } = this.props;
return (
<BBBMenu
@ -340,6 +340,16 @@ class UserOptions extends PureComponent {
/>
)}
actions={this.renderMenuItems()}
opts={{
id: "default-dropdown-menu",
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getContentAnchorEl: null,
fullwidth: "true",
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
}}
/>
);
}

View File

@ -10,6 +10,7 @@ import logger from '/imports/startup/client/logger';
import { defineMessages, injectIntl } from 'react-intl';
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,
@ -60,6 +61,8 @@ const UserOptionsContainer = withTracker((props) => {
return name;
};
const isRTL = layoutSelect((i) => i.isRTL);
return {
toggleMuteAllUsers: () => {
UserListService.muteAllUsers(Auth.userID);
@ -91,6 +94,7 @@ const UserOptionsContainer = withTracker((props) => {
meetingName: getMeetingName(),
openLearningDashboardUrl: LearningDashboardService.openLearningDashboardUrl,
dynamicGuestPolicy,
isRTL,
};
})(UserOptions);

View File

@ -151,6 +151,7 @@ const JoinVideoButton = ({
emoji="device_list_selector"
hideLabel
label={intl.formatMessage(intlMessages.videoSettings)}
rotate
/>
)}
actions={actions}

View File

@ -23,7 +23,7 @@ const VideoListItem = (props) => {
const {
name, voiceUser, isFullscreenContext, layoutContextDispatch, user, onHandleVideoFocus,
cameraId, numOfStreams, focused, onVideoItemMount, onVideoItemUnmount, onVirtualBgDrop,
makeDragOperations,
makeDragOperations, isRTL
} = props;
const [videoIsReady, setVideoIsReady] = useState(false);
@ -203,6 +203,50 @@ const VideoListItem = (props) => {
animations={animations}
{...makeDragOperations(onVirtualBgDrop, user.userId)}
>
{
videoIsReady
? (
<>
<Styled.TopBar>
<PinArea
user={user}
/>
<ViewActions
videoContainer={videoContainer}
name={name}
cameraId={cameraId}
isFullscreenContext={isFullscreenContext}
layoutContextDispatch={layoutContextDispatch}
/>
</Styled.TopBar>
<Styled.BottomBar>
<UserActions
name={name}
user={user}
cameraId={cameraId}
numOfStreams={numOfStreams}
onHandleVideoFocus={onHandleVideoFocus}
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isRTL={isRTL}
/>
<UserStatus
voiceUser={voiceUser}
/>
</Styled.BottomBar>
</>
)
: (
<Styled.WebcamConnecting
data-test="webcamConnecting"
talking={talking}
animations={animations}
>
<Styled.LoadingText>{name}</Styled.LoadingText>
</Styled.WebcamConnecting>
)
}
<Styled.VideoContainer>
<Styled.Video
mirrored={isMirrored}

View File

@ -13,6 +13,7 @@ const VideoListItemContainer = (props) => {
const { element } = fullscreen;
const isFullscreenContext = (element === cameraId);
const layoutContextDispatch = layoutDispatch();
const isRTL = layoutSelect((i) => i.isRTL);
return (
<VideoListItem
@ -20,6 +21,7 @@ const VideoListItemContainer = (props) => {
{...{
isFullscreenContext,
layoutContextDispatch,
isRTL,
}}
/>
);

View File

@ -51,7 +51,7 @@ const intlMessages = defineMessages({
const UserActions = (props) => {
const {
name, cameraId, numOfStreams, onHandleVideoFocus, user, focused, onHandleMirror,
isVideoSqueezed, videoContainer,
isVideoSqueezed, videoContainer, isRTL
} = props;
const intl = useIntl();
@ -154,8 +154,8 @@ const UserActions = (props) => {
elevation: 3,
getContentAnchorEl: null,
fullwidth: 'true',
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
transformorigin: { vertical: 'bottom', horizontal: 'left' },
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
}}
/>
)

View File

@ -8,6 +8,7 @@ import Styled from './styles';
import { PANELS, ACTIONS } from '../layout/enums';
import Settings from '/imports/ui/services/settings';
import browserInfo from '/imports/utils/browserInfo';
import Header from '/imports/ui/components/common/control-header/component';
const intlMessages = defineMessages({
waitingUsersTitle: {
@ -314,15 +315,12 @@ const WaitingUsers = (props) => {
return (
<Styled.Panel data-test="note" isChrome={isChrome}>
<Styled.Header>
<Styled.Title data-test="noteTitle">
<Styled.HideButton
onClick={() => closePanel()}
label={intl.formatMessage(intlMessages.title)}
icon="left_arrow"
/>
</Styled.Title>
</Styled.Header>
<Header
leftButtonProps={{
onClick: () => closePanel(),
label: intl.formatMessage(intlMessages.title),
}}
/>
<Styled.ScrollableArea>
{isGuestLobbyMessageEnabled ? (
<Styled.LobbyMessage>

View File

@ -5,21 +5,16 @@ import {
itemFocusBorder,
colorGray,
colorWhite,
colorGrayDark,
colorGrayLightest,
colorOffWhite,
} from '/imports/ui/stylesheets/styled-components/palette';
import {
borderSize,
borderSizeLarge,
mdPaddingX,
mdPaddingY,
pollHeaderOffset,
toastContentWidth,
} from '/imports/ui/stylesheets/styled-components/general';
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import { DivElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
import Button from '/imports/ui/components/common/button/component';
import { ScrollboxVertical } from '/imports/ui/stylesheets/styled-components/scrollable';
@ -150,55 +145,6 @@ const Panel = styled.div`
}
`;
const Header = styled.header`
position: relative;
top: ${pollHeaderOffset};
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
`;
const Title = styled(DivElipsis)`
flex: 1;
& > button, & > button:hover {
max-width: ${toastContentWidth};
}
`;
const HideButton = styled(Button)`
position: relative;
background-color: ${colorWhite};
display: block;
margin: ${borderSizeLarge};
margin-bottom: ${borderSize};
padding-left: 0;
padding-right: inherit;
[dir="rtl"] & {
padding-left: inherit;
padding-right: 0;
}
& > i {
color: ${colorGrayDark};
font-size: smaller;
[dir="rtl"] & {
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}
&:hover {
background-color: ${colorWhite};
}
`;
const LobbyMessage = styled.div`
border-bottom: 1px solid ${colorGrayLightest};
margin: 2px 2px 0 2px;
@ -266,9 +212,6 @@ export default {
Users,
CustomButton,
Panel,
Header,
Title,
HideButton,
LobbyMessage,
RememberContainer,
ScrollableArea,

View File

@ -24,6 +24,7 @@ const WebcamComponent = ({
const [isFullscreen, setIsFullScreen] = useState(false);
const [resizeStart, setResizeStart] = useState({ width: 0, height: 0 });
const [cameraMaxWidth, setCameraMaxWidth] = useState(0);
const [draggedAtLeastOneTime, setDraggedAtLeastOneTime] = useState(false);
const lastSize = Storage.getItem('webcamSize') || { width: 0, height: 0 };
const { width: lastWidth, height: lastHeight } = lastSize;
@ -119,9 +120,10 @@ const WebcamComponent = ({
const handleWebcamDragStop = (e) => {
setIsDragging(false);
setDraggedAtLeastOneTime(false);
document.body.style.overflow = 'auto';
if (Object.values(CAMERADOCK_POSITION).includes(e.target.id)) {
if (Object.values(CAMERADOCK_POSITION).includes(e.target.id) && draggedAtLeastOneTime) {
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_POSITION,
value: e.target.id,
@ -160,6 +162,11 @@ const WebcamComponent = ({
handle="video"
bounds="html"
onStart={handleWebcamDragStart}
onDrag={() => {
if (!draggedAtLeastOneTime) {
setDraggedAtLeastOneTime(true);
}
}}
onStop={handleWebcamDragStop}
onMouseDown={
cameraDock.isDraggable ? (e) => e.preventDefault() : undefined

View File

@ -81,14 +81,6 @@ const ToolbarButton = styled(Button)`
& > i {
color: ${toolbarListColor};
}
border-top-left-radius: 0;
border-top-right-radius: ${toolbarButtonBorderRadius};
[dir="rtl"] & {
border-top-left-radius: ${toolbarButtonBorderRadius};
border-top-right-radius: 0;
}
`}
`;

View File

@ -32,6 +32,13 @@ const ButtonWrapper = styled.div`
&:first-child > button {
border-top-left-radius: ${toolbarButtonBorderRadius};
border-bottom-left-radius: ${toolbarButtonBorderRadius};
[dir="rtl"] & {
border-top-right-radius: ${toolbarButtonBorderRadius};
border-bottom-right-radius: ${toolbarButtonBorderRadius};
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
`;

View File

@ -58,10 +58,6 @@ class Auth {
Storage.setItem('sessionToken', this._sessionToken);
}
get sessionToken() {
return this._sessionToken;
}
get userID() {
return this._userID;
}
@ -151,15 +147,6 @@ class Auth {
};
}
set _connectionID(connectionId) {
this._connectionID = connectionId;
Storage.setItem('sessionToken', this._connectionID);
}
get sessionToken() {
return this._sessionToken;
}
set(
meetingId,
requesterUserId,
@ -253,6 +240,7 @@ class Auth {
initAnnotationsStreamListener();
clearTimeout(validationTimeout);
this.connectionID = authenticationTokenValidation.connectionId;
this.connectionAuthTime = new Date().getTime();
Session.set('userWillAuth', false);
setTimeout(() => resolve(true), 100);
break;

View File

@ -26,6 +26,10 @@ const GlobalStyle = createGlobalStyle`
}
}
.MuiList-padding {
padding: 0 !important;
}
.MuiPaper-root {
background-color: ${dropdownBg};
border-radius: ${borderRadius};

View File

@ -1,7 +1,5 @@
const TITLE_WITH_VIEW = 3;
const ARIA_ALERT_TIMEOUT = 3000;
const ARIA_ALERT_EXT_TIMEOUT = 15000;
const getTitleData = () => {
const title = document.getElementsByTagName('title')[0];
@ -23,27 +21,7 @@ export const unregisterTitleView = () => {
title.text = data.join(' - ');
};
export const alertScreenReader = (s = '') => {
const app = document.getElementById('app');
const ariaAlert = document.createElement("div");
ariaAlert.setAttribute("id", "aria-alert");
ariaAlert.setAttribute("role", "alert");
ariaAlert.setAttribute("aria-hidden", false);
ariaAlert.setAttribute("className", "sr-only");
ariaAlert.textContent = s;
app.appendChild(ariaAlert);
setTimeout(() => {
document.getElementById('aria-alert').remove();
}, ARIA_ALERT_TIMEOUT);
export default {
registerTitleView,
unregisterTitleView,
};
export const politeSRAlert = (s = '') => {
const liveArea = document.getElementById('aria-polite-alert')
if (liveArea) liveArea.innerHTML = s;
setTimeout(() => {
if (liveArea) liveArea.innerHTML = '';
}, ARIA_ALERT_EXT_TIMEOUT);
};
export default { registerTitleView, unregisterTitleView, alertScreenReader, politeSRAlert };

View File

@ -1,3 +1,40 @@
export const capitalizeFirstLetter = (s = '') => s.charAt(0).toUpperCase() + s.slice(1);
export default { capitalizeFirstLetter };
import sanitize from 'sanitize-html';
export const capitalizeFirstLetter = (s = '') => s.charAt(0).toUpperCase() + s.slice(1);
/**
* Returns a string in the format 'Year-Month-Day_Hour-Minutes'.
* @param {Date} [date] - The Date object.
*/
export const getDateString = (date = new Date()) => {
const hours = date.getHours().toString().padStart(2, 0);
const minutes = date.getMinutes().toString().padStart(2, 0);
const month = (date.getMonth() + 1).toString().padStart(2, 0);
const dayOfMonth = date.getDate().toString().padStart(2, 0);
const time = `${hours}-${minutes}`;
const dateString = `${date.getFullYear()}-${month}-${dayOfMonth}_${time}`;
return dateString;
};
export const stripTags = (text) => sanitize(text, { allowedTags: [] });
// Sanitize. See: https://gist.github.com/sagewall/47164de600df05fb0f6f44d48a09c0bd
export const escapeHtml = (text) => {
const div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
};
export const unescapeHtml = (input) => {
const e = document.createElement('textarea');
e.innerHTML = input;
return e.value;
};
export default {
capitalizeFirstLetter,
getDateString,
stripTags,
escapeHtml,
unescapeHtml,
};

View File

@ -261,8 +261,6 @@
updateMessage(_('app.guest.guestWait'));
enableAnimation();
try {
const ATTEMPT_EVERY_MS = 10 * 1000; // 10 seconds
const sessionToken = getSearchParam('sessionToken');
if (!sessionToken) {
@ -270,8 +268,32 @@
updateMessage(_('app.guest.noSessionToken'));
return;
}
//First, check that we already have a response
const statusFromStorage = sessionStorage.getItem(`guestStatus_${sessionToken}`);
pollGuestStatus(sessionToken, ATTEMPT_EVERY_MS);
if(statusFromStorage) {
stopUpdatingWaitingPosition();
const statusParsed = JSON.parse(statusFromStorage);
const { status, response } = statusParsed;
if(status === 'ALLOW'){
updateLobbyMessage(_('app.guest.allow'));
setTimeout(() => {
disableAnimation();
window.location = response.url;
}, MESSAGE_TIMEOUT);
} else {
redirect(
_('app.guest.' + response.messageKey),
response.url,
);
}
return;
}
pollGuestStatus(sessionToken, 0);
} catch (e) {
disableAnimation();
console.error(e);
@ -309,21 +331,28 @@
.then((data) => {
const code = data.response.returncode;
if (code === 'FAILED') {
const response = data.response;
const saveStatusResponse = (status, response, token) => {
stopUpdatingWaitingPosition();
sessionStorage.setItem(`guestStatus_${token}`, JSON.stringify({ status, response }));
};
if (code === 'FAILED') {
saveStatusResponse(code, response, token);
return redirect(_('app.guest.' + data.response.messageKey), data.response.url);
}
const status = data.response.guestStatus;
if (status === 'DENY') {
stopUpdatingWaitingPosition();
saveStatusResponse(status, response, token);
return redirect(_('app.guest.' + data.response.messageKey), data.response.url);
}
if (status === 'ALLOW') {
updateLobbyMessage(_('app.guest.allow'));
stopUpdatingWaitingPosition();
saveStatusResponse(status, response, token);
// Timeout is required by accessibility to allow viewing of the message for a minimum of 3 seconds
// before redirecting.
setTimeout(() => {
@ -335,7 +364,8 @@
updatePositionInWaitingQueue(data.response.positionInWaitingQueue);
updateLobbyMessage(data.response.lobbyMessage);
return pollGuestStatus(token, everyMs);
const ATTEMPT_EVERY_MS = 10 * 1000; // 10 seconds
return pollGuestStatus(token, ATTEMPT_EVERY_MS);
});
}, everyMs);
};

View File

@ -111,7 +111,7 @@
"app.userList.userOptions.unmuteAllLabel": "إيقاف كتم صوت الاجتماع",
"app.userList.userOptions.unmuteAllDesc": "إلغاء كتم صوت الاجتماع",
"app.userList.userOptions.lockViewersLabel": "قفل المشاهدين",
"app.userList.userOptions.lockViewersDesc": "قفل وظائف معينة على اامشاركين في الاجتماع",
"app.userList.userOptions.lockViewersDesc": "قفل وظائف معينة على المشاركين في الاجتماع",
"app.userList.userOptions.guestPolicyLabel": "سياسة الضيف",
"app.userList.userOptions.guestPolicyDesc": "تغيير إعداد سياسة الضيف في الاجتماع",
"app.userList.userOptions.disableCam": "كاميرات المشاهدين معطلة",
@ -444,7 +444,7 @@
"app.actionsBar.actionsDropdown.presentationLabel": "إدارة العروض",
"app.actionsBar.actionsDropdown.initPollLabel": "إنشاء تصويت جديد",
"app.actionsBar.actionsDropdown.desktopShareLabel": "مشاركة الشاشة",
"app.actionsBar.actionsDropdown.lockedDesktopShareLabel": "مشاركة الشاشة مقفل",
"app.actionsBar.actionsDropdown.lockedDesktopShareLabel": "مشاركة الشاشة مقفلة",
"app.actionsBar.actionsDropdown.stopDesktopShareLabel": "إيقاف مشاركة الشاشة",
"app.actionsBar.actionsDropdown.presentationDesc": "إرفع عرضك",
"app.actionsBar.actionsDropdown.initPollDesc": "إنشاء تصويت جديد",
@ -501,17 +501,17 @@
"app.audioNotification.audioFailedMessage": "فشل اتصال الصوت الخاص بك في الاتصال",
"app.audioNotification.mediaFailedMessage": "فشل getUserMicMedia ، مسموح فقط للأصول الآمنة",
"app.audioNotification.closeLabel": "غلق",
"app.audioNotificaion.reconnectingAsListenOnly": "تم قفل صوت المشاهدين، حيث يتم اتصالك بالاستماع فقط",
"app.audioNotificaion.reconnectingAsListenOnly": "تم قفل صوت المشاهدين، يتم اتصالك بالاستماع فقط",
"app.breakoutJoinConfirmation.title": "انضم إلى الغرفة الجانبية",
"app.breakoutJoinConfirmation.message": "هل تود الانضمام",
"app.breakoutJoinConfirmation.confirmDesc": "انضم إليكم إلى الغرفة الجانبية",
"app.breakoutJoinConfirmation.dismissLabel": "إلغاء",
"app.breakoutJoinConfirmation.dismissDesc": "يغلق ويرفض الانضمام إلى الغرفة الجانبية",
"app.breakoutJoinConfirmation.freeJoinMessage": "اختر الغرفة الجانبية للانضمام",
"app.breakoutTimeRemainingMessage": "وقت انتهاء الغرفة المفرقة: {0}",
"app.breakoutTimeRemainingMessage": "وقت انتهاء الغرفة الجانبية: {0}",
"app.breakoutWillCloseMessage": "انتهى الوقت. سيتم إغلاق الغرفة الجانبية قريبًا",
"app.breakout.dropdown.manageDuration": "إدارة المدة",
"app.breakout.dropdown.destroyAll": "تدمير الجانبيات",
"app.breakout.dropdown.manageDuration": "تغيير المدة",
"app.breakout.dropdown.destroyAll": "إنهاء الغرف الجانبية",
"app.breakout.dropdown.options": "خيارات الجانبيات",
"app.calculatingBreakoutTimeRemaining": "حساب الوقت المتبقي ...",
"app.audioModal.ariaTitle": "نافذة الانضمام إلى الصوت",
@ -607,6 +607,7 @@
"app.error.500": "عفوا، حدث خطأ ما",
"app.error.userLoggedOut": "المستخدم لديه رمز جلسة غير صالح بسبب تسجيل الخروج",
"app.error.ejectedUser": "المستخدم لديه رمز جلسة غير صالح بسبب الطرد",
"app.error.joinedAnotherWindow": "يبدو أن هذه الجلسة مفتوحة في نافذة متصفح أخر.",
"app.error.userBanned": "تم حظر المستخدم",
"app.error.leaveLabel": "تسجيل الدخول مرة أخرى",
"app.error.fallback.presentation.title": "حدث خطأ",
@ -793,7 +794,7 @@
"app.video.cancel": "إلغاء",
"app.video.swapCam": "مبادلة",
"app.video.swapCamDesc": "مبادلة اتجاه الكاميرات",
"app.video.videoLocked": "مشاركة الكامرا مقفل",
"app.video.videoLocked": "مشاركة الكامرا مقفلة",
"app.video.videoButtonDesc": "مشاركة كاميرا",
"app.video.videoMenu": "قائمة الفيديو",
"app.video.videoMenuDisabled": "تم تعطيل قائمة كاميرا الفيديو في الإعدادات",
@ -904,7 +905,7 @@
"app.createBreakoutRoom.resetAssignmentsDesc": "إعادة تعيين جميع تعيينات غرفة المستخدم",
"app.createBreakoutRoom.endAllBreakouts": "إنهاء جميع الغرف الجانبية",
"app.createBreakoutRoom.chatTitleMsgAllRooms": "كل الغرف",
"app.createBreakoutRoom.msgToBreakoutsSent": "تم إرسال الرسالة إلى {0} غرفة فرعية",
"app.createBreakoutRoom.msgToBreakoutsSent": "تم إرسال الرسالة إلى {0} غرفة جانبية",
"app.createBreakoutRoom.roomName": "{0} (غرفة- {1})",
"app.createBreakoutRoom.doneLabel": "تم",
"app.createBreakoutRoom.nextLabel": "التالي",

View File

@ -508,8 +508,6 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "Escull la sala separada per a unir-se",
"app.breakoutTimeRemainingMessage": "Temps restant a la sala separada: {0}",
"app.breakoutWillCloseMessage": "Temps finalitzat. La reunió separada es tancarà aviat",
"app.breakout.dropdown.manageDuration": "Gestionar la durada",
"app.breakout.dropdown.destroyAll": "Esborrar sales externes",
"app.breakout.dropdown.options": "Opcions de sales externes",
"app.calculatingBreakoutTimeRemaining": "Calculant temps restant ...",
"app.audioModal.ariaTitle": "Entra a l'àudio modal",

View File

@ -35,7 +35,7 @@
"app.captions.menu.ariaStartDesc": "Öffnet den Untertiteleditor und schließt diesen Dialog",
"app.captions.menu.select": "Verfügbare Sprache auswählen",
"app.captions.menu.ariaSelect": "Untertitelsprache",
"app.captions.menu.subtitle": "Bitte eine Sprache und einen Schriftstil für die Untertitel der Konferenz wählen.",
"app.captions.menu.subtitle": "Bitte wählen Sie eine Sprache und einen Schriftstil für die Untertitel Ihrer Konferenz.",
"app.captions.menu.title": "Untertitel",
"app.captions.menu.fontSize": "Größe",
"app.captions.menu.fontColor": "Schriftfarbe",
@ -62,7 +62,7 @@
"app.note.converter-button.convertAndUpload": "Geteilte Notizen im Präsentations-Bereich laden",
"app.pads.hint": "Esc drücken, um die Symbolleiste des Pads auszuwählen",
"app.user.activityCheck": "Teilnehmeraktivitätsprüfung",
"app.user.activityCheck.label": "Prüfen, ob der:die Teilnehmer:in noch in der Konferenz ist ({0})",
"app.user.activityCheck.label": "Prüfen, ob der Teilnehmer noch in der Konferenz ist ({0})",
"app.user.activityCheck.check": "Prüfen",
"app.userList.usersTitle": "Teilnehmer",
"app.userList.participantsTitle": "Teilnehmer",
@ -87,11 +87,11 @@
"app.userList.menu.clearStatus.label": "Status zurücksetzen",
"app.userList.menu.removeUser.label": "Teilnehmer entfernen",
"app.userList.menu.removeConfirmation.label": "Teilnehmer entfernen ({0})",
"app.userlist.menu.removeConfirmation.desc": "Teilnehmer:in sperren, sodass eine erneute Teilnahme an dieser Konferenz nicht mehr möglich ist.",
"app.userlist.menu.removeConfirmation.desc": "Teilnehmer sperren, sodass eine erneute Teilnahme an dieser Konferenz nicht mehr möglich ist.",
"app.userList.menu.muteUserAudio.label": "Teilnehmer stummschalten",
"app.userList.menu.unmuteUserAudio.label": "Stummschaltung aufheben",
"app.userList.menu.webcamPin.label": "Webcam der Teilnehmer:in fixieren",
"app.userList.menu.webcamUnpin.label": "Webcam der Teilnehmer:in lösen",
"app.userList.menu.webcamPin.label": "Webcam der Teilnehmer anheften",
"app.userList.menu.webcamUnpin.label": "Webcam der Teilnehmer lösen",
"app.userList.menu.giveWhiteboardAccess.label" : "Zugriff auf Whiteboard erlauben",
"app.userList.menu.removeWhiteboardAccess.label": "Zugriff auf Whiteboard aufheben",
"app.userList.menu.ejectUserCameras.label": "Kameras schließen",
@ -104,7 +104,7 @@
"app.userList.menu.makePresenter.label": "Zum Präsentator machen",
"app.userList.userOptions.manageUsersLabel": "Teilnehmer verwalten",
"app.userList.userOptions.muteAllLabel": "Alle Teilnehmer stummschalten",
"app.userList.userOptions.muteAllDesc": "Alle Teilnehmer:innen der Konferenz werden stumm geschaltet",
"app.userList.userOptions.muteAllDesc": "Alle Teilnehmer der Konferenz werden stumm geschaltet",
"app.userList.userOptions.clearAllLabel": "Alle Statusicons löschen",
"app.userList.userOptions.clearAllDesc": "Alle Statusicons der Teilnehmer löschen",
"app.userList.userOptions.muteAllExceptPresenterLabel": "Alle Teilnehmer außer den Präsentator stummschalten",
@ -112,7 +112,7 @@
"app.userList.userOptions.unmuteAllLabel": "Konferenz-Stummschaltung aufheben",
"app.userList.userOptions.unmuteAllDesc": "Hebt die Konferenz-Stummschaltung auf",
"app.userList.userOptions.lockViewersLabel": "Teilnehmerrechte einschränken",
"app.userList.userOptions.lockViewersDesc": "Schränkt bestimmte Funktionen der Konferenzteilnehmer:innen ein",
"app.userList.userOptions.lockViewersDesc": "Schränkt bestimmte Funktionen der Konferenzteilnehmer ein",
"app.userList.userOptions.guestPolicyLabel": "Gastzugang regeln",
"app.userList.userOptions.guestPolicyDesc": "Grundregel für den Gastzugang ändern",
"app.userList.userOptions.disableCam": "Teilnehmerwebcams sind deaktiviert",
@ -120,21 +120,21 @@
"app.userList.userOptions.disablePrivChat": "Privater Chat ist deaktiviert",
"app.userList.userOptions.disablePubChat": "Öffentlicher Chat ist deaktiviert",
"app.userList.userOptions.disableNotes": "Geteilte Notizen sind jetzt gesperrt",
"app.userList.userOptions.hideUserList": "Liste der Teilnehmer:innen ist jetzt für diese ausgeblendet",
"app.userList.userOptions.hideUserList": "Teilnehmerliste ist jetzt für die Teilnehmer ausgeblendet",
"app.userList.userOptions.webcamsOnlyForModerator": "Nur Moderatoren können die Teilnehmerwebcams sehen (wegen eingeschränkter Rechteeinstellungen)",
"app.userList.content.participants.options.clearedStatus": "Status aller Teilnehmer:innen zurückgesetzt",
"app.userList.content.participants.options.clearedStatus": "Status aller Teilnehmer zurückgesetzt",
"app.userList.userOptions.enableCam": "Teilnehmer dürfen ihre Webcams verwenden",
"app.userList.userOptions.enableMic": "Teilnehmer dürfen ihre Mikrofone verwenden",
"app.userList.userOptions.enablePrivChat": "Privater Chat ist erlaubt",
"app.userList.userOptions.enablePubChat": "Gemeinsamer Chat ist erlaubt",
"app.userList.userOptions.enablePubChat": "Öffentlicher Chat ist erlaubt",
"app.userList.userOptions.enableNotes": "Geteilte Notizen sind jetzt erlaubt",
"app.userList.userOptions.showUserList": "Liste der Teilnehmer:innen ist für diese sichtbar",
"app.userList.userOptions.enableOnlyModeratorWebcam": "Webcam nun freigeben um für jede:n sichtbar zu sein.",
"app.userList.userOptions.showUserList": "Teilnehmerliste ist jetzt für die Teilnehmer sichtbar",
"app.userList.userOptions.enableOnlyModeratorWebcam": "Sie können Ihre Webcam jetzt freigeben, jeder wird Sie sehen.",
"app.userList.userOptions.savedNames.title": "Liste der Teilnehmer in der Konferenz {0} vom {1}",
"app.userList.userOptions.sortedFirstName.heading": "Sortiert nach Vorname:",
"app.userList.userOptions.sortedLastName.heading": "Sortiert nach Nachname:",
"app.userList.userOptions.hideViewersCursor": "Cursor der Teilnehmer:innen sind gesperrt",
"app.userList.userOptions.showViewersCursor": "Cursor der Teilnehmer:innen sind freigegeben",
"app.userList.userOptions.hideViewersCursor": "Cursor der Teilnehmer sind gesperrt",
"app.userList.userOptions.showViewersCursor": "Cursor der Teilnehmer sind freigegeben",
"app.media.label": "Media",
"app.media.autoplayAlertDesc": "Zugang erlauben",
"app.media.screenshare.start": "Bildschirmfreigabe wurde gestartet",
@ -142,7 +142,7 @@
"app.media.screenshare.endDueToDataSaving": "Bildschirmübertragung im Datensparmodus gestoppt",
"app.media.screenshare.unavailable": "Bildschirmfreigabe nicht verfügbar",
"app.media.screenshare.notSupported": "Bildschirmfreigabe wird in diesem Browser nicht unterstützt.",
"app.media.screenshare.autoplayBlockedDesc": "Wir benötigen deine/Ihre Zustimmung, um den Bildschirm des Präsentators zu zeigen.",
"app.media.screenshare.autoplayBlockedDesc": "Wir benötigen Ihre Zustimmung, um den Bildschirm des Präsentators zu zeigen.",
"app.media.screenshare.autoplayAllowLabel": "Geteilten Bildschirm ansehen",
"app.screenshare.presenterLoadingLabel": "Ihr Bildschirm wird freigegeben",
"app.screenshare.viewerLoadingLabel": "Der Bildschirm des Präsentators wird geladen",
@ -150,14 +150,14 @@
"app.screenshare.screenshareFinalError": "Fehler {0}. Bildschirm konnte nicht geteilt werden.",
"app.screenshare.screenshareRetryError": "Fehler {0}. Bitte versuchen, den Bildschirm erneut zu teilen.",
"app.screenshare.screenshareRetryOtherEnvError": "Fehler {0}. Der Bildschirm konnte nicht freigegeben werden. Bitte mit einem anderen Browser oder einem anderen Gerät probieren.",
"app.screenshare.screenshareUnsupportedEnv": "Fehler {0}. Dieser Browser wird nicht unterstützt. Bitte einen anderen Browser oder ein anderes Endgerät probieren.",
"app.screenshare.screenshareUnsupportedEnv": "Fehler {0}. Dieser Browser wird nicht unterstützt. Bitte einen anderen Browser oder ein anderes Gerät probieren.",
"app.screenshare.screensharePermissionError": "Fehler {0}. Die Berechtigung zur Bildschirmfreigabe muss erteilt werden.",
"app.meeting.ended": "Diese Konferenz wurde beendet",
"app.meeting.meetingTimeRemaining": "Verbleibende Konferenzzeit: {0}",
"app.meeting.meetingTimeHasEnded": "Die Zeit ist abgelaufen. Die Konferenz wird in Kürze beendet",
"app.meeting.endedByUserMessage": "Diese Konferenz wurde durch {0} beendet",
"app.meeting.endedByNoModeratorMessageSingular": "Die Konferenz ist beendet, da nach einer Minute kein:e Moderator:in anwesend ist",
"app.meeting.endedByNoModeratorMessagePlural": "Die Konferenz ist beendet, da nach {0} Minuten kein:e Moderator:in anwesend ist",
"app.meeting.endedByNoModeratorMessageSingular": "Die Konferenz ist beendet, da nach einer Minute kein Moderator anwesend ist",
"app.meeting.endedByNoModeratorMessagePlural": "Die Konferenz ist beendet, da nach {0} Minuten kein Moderator anwesend ist",
"app.meeting.endedMessage": "Sie werden zum Startbildschirm weitergeleitet",
"app.meeting.alertMeetingEndsUnderMinutesSingular": "Konferenz endet in einer Minute.",
"app.meeting.alertMeetingEndsUnderMinutesPlural": "Konferenz endet in {0} Minuten.",
@ -204,8 +204,8 @@
"app.presentation.presentationToolbar.goToSlide": "Folie {0}",
"app.presentation.placeholder": "Es gibt derzeit keine aktive Präsentation",
"app.presentationUploder.title": "Präsentation",
"app.presentationUploder.message": "Präsentationen können als Office-Dokumente oder PDF-Dateien hochgeladen werden. PDF-Dateien haben dabei die bessere Qualität. Eine Präsentation muss dann durch das runde Markierungsfeld auf der rechten Seite ausgewählt sein.",
"app.presentationUploder.extraHint": "Jede Datei darf {0} MB und {1} Seiten nicht überschreiten.",
"app.presentationUploder.message": "Als Präsentator können Präsentationen als Office-Dokumente oder PDF-Dateien hochgeladen werden. PDF-Dateien haben dabei die bessere Qualität. Bitte achten Sie darauf, dass eine Präsentation über das runde Auswahlfeld auf der rechten Seite ausgewählt ist.",
"app.presentationUploder.extraHint": "WICHTIG: Jede Datei darf {0} MB und {1} Seiten nicht überschreiten.",
"app.presentationUploder.uploadLabel": "Hochladen",
"app.presentationUploder.confirmLabel": "Bestätigen",
"app.presentationUploder.confirmDesc": "Änderungen speichern und Präsentation starten",
@ -257,12 +257,12 @@
"app.poll.autoOptionInstructions.label": "Automatische Optionierung ist aktiviert schreiben Sie die Umfragefrage und Option(en) im angegebenen Format.",
"app.poll.maxOptionsWarning.label": "Nur die ersten 5 Optionen können verwendet werden!",
"app.poll.pollPaneTitle": "Umfrage",
"app.poll.enableMultipleResponseLabel": "Mehrere Antworten pro Befragte:n zulassen?",
"app.poll.enableMultipleResponseLabel": "Mehrere Antworten pro Befragten zulassen?",
"app.poll.quickPollTitle": "Schnellumfrage",
"app.poll.hidePollDesc": "Versteckt das Umfragemenü",
"app.poll.quickPollInstruction": "Bitte eine der unten stehenden Optionen wählen, um die Umfrage zu starten.",
"app.poll.activePollInstruction": "Dieses Fenster offen lassen, um auf die Antworten der Teilnehmer:innen zu warten. Sobald auf 'Umfrageergebnisse veröffentlichen' geklickt wird, werden die Ergebnisse angezeigt und wird die Umfrage beendet.",
"app.poll.dragDropPollInstruction": "Um die Umfrageoptionen automatisch auszufüllen, eine Textdatei mit den Umfrageoptionen per Drag&Drop in das hervorgehobene Feld ziehen",
"app.poll.quickPollInstruction": "Wählen Sie eine der unten stehenden Optionen, um die Umfrage zu starten.",
"app.poll.activePollInstruction": "Lassen Sie dieses Fenster offen, um auf die Antworten der Teilnehmer zu warten. Sobald Sie auf 'Umfrageergebnisse veröffentlichen' klicken, werden die Ergebnisse angezeigt und die Umfrage beendet.",
"app.poll.dragDropPollInstruction": "Um die Umfrageoptionen automatisch auszufüllen, eine Textdatei mit den Umfrageoptionen per Drag&Drop in das hervorgehobene Feld ziehen",
"app.poll.customPollTextArea": "Umfrageoptionen ausfüllen",
"app.poll.publishLabel": "Umfrage veröffentlichen",
"app.poll.cancelPollLabel": "Abbrechen",
@ -273,13 +273,13 @@
"app.poll.customPlaceholder": "Umfrageoption hinzufügen",
"app.poll.noPresentationSelected": "Keine Präsentation ausgewählt! Bitte eine auswählen.",
"app.poll.clickHereToSelect": "Zum Auswählen hier klicken",
"app.poll.question.label" : "Eine Frage stellen ...",
"app.poll.optionalQuestion.label" : "Eine Frage stellen (optional) ...",
"app.poll.question.label" : "Eine Frage stellen...",
"app.poll.optionalQuestion.label" : "Eine Frage stellen (optional)...",
"app.poll.userResponse.label" : "Teilnehmerantwort",
"app.poll.responseTypes.label" : "Antworttypen",
"app.poll.optionDelete.label" : "Löschen",
"app.poll.responseChoices.label" : "Antwortmöglichkeiten",
"app.poll.typedResponse.desc" : "Den Teilnehmer:innen wird ein Textfeld angezeigt, um die Antwort einzutragen.",
"app.poll.typedResponse.desc" : "Den Teilnehmern wird ein Textfeld angezeigt, um die Antwort einzutragen.",
"app.poll.addItem.label" : "Element hinzufügen",
"app.poll.start.label" : "Umfrage starten",
"app.poll.secretPoll.label" : "Anonyme Umfrage",
@ -330,7 +330,7 @@
"app.connectingMessage": "Verbinde...",
"app.waitingMessage": "Verbindung unterbrochen. Versuche in {0} Sekunden erneut zu verbinden...",
"app.retryNow": "Jetzt erneut versuchen",
"app.muteWarning.label": "Auf {0} klicken, um die Stummschaltung aufzuheben.",
"app.muteWarning.label": "Auf {0} klicken, um Ihre Stummschaltung aufzuheben.",
"app.muteWarning.disableMessage": "Hinweis auf Stummschaltung deaktiviert, bis die Stummschaltung aufgehoben wird",
"app.muteWarning.tooltip": "Klicken, um den Hinweis bis zur nächsten Aufhebung der Stummschaltung zu schließen",
"app.navBar.settingsDropdown.optionsLabel": "Optionen",
@ -361,7 +361,7 @@
"app.leaveConfirmation.confirmLabel": "Verlassen",
"app.leaveConfirmation.confirmDesc": "Hiermit verlassen Sie die Konferenz",
"app.endMeeting.title": "{0} beenden",
"app.endMeeting.description": "Mit dieser Aktion wird die Sitzung für {0} aktive(n) Teilnehmer:innen beendet. Sicher, dass die Sitzung beendet werden soll?",
"app.endMeeting.description": "Mit dieser Aktion wird die Konferenz für {0} aktive(n) Teilnehmer beendet. Sind Sie sicher, dass Sie diese Konferenz beenden möchten?",
"app.endMeeting.noUserDescription": "Sind Sie sicher, dass Sie die Konferenz beenden wollen?",
"app.endMeeting.contentWarning": "Chatnachrichten, geteilte Notizen, Whiteboard-Inhalte und geteilte Dokumente dieser Konferenz sind nicht mehr direkt zugänglich",
"app.endMeeting.yesLabel": "Ja",
@ -436,7 +436,7 @@
"app.statusNotifier.and": "und",
"app.switch.onLabel": "AN",
"app.switch.offLabel": "AUS",
"app.talkingIndicator.ariaMuteDesc" : "Auswählen, um Teilnehmer:in stummzuschalten",
"app.talkingIndicator.ariaMuteDesc" : "Auswählen, um Teilnehmer stummzuschalten",
"app.talkingIndicator.isTalking" : "{0} spricht",
"app.talkingIndicator.moreThanMaxIndicatorsTalking" : "{0}+ sprechen",
"app.talkingIndicator.moreThanMaxIndicatorsWereTalking" : "{0}+ sprachen",
@ -460,8 +460,8 @@
"app.actionsBar.actionsDropdown.captionsDesc": "Untertitelfenster umschalten",
"app.actionsBar.actionsDropdown.takePresenter": "Zum Präsentator werden",
"app.actionsBar.actionsDropdown.takePresenterDesc": "Sich selbst zum neuen Präsentator machen",
"app.actionsBar.actionsDropdown.selectRandUserLabel": "Zufällig auswählen",
"app.actionsBar.actionsDropdown.selectRandUserDesc": "Wählt eine:n Teilnehmer:in nach dem Zufallsprinzip aus.",
"app.actionsBar.actionsDropdown.selectRandUserLabel": "Zufälligen Teilnehmer auswählen",
"app.actionsBar.actionsDropdown.selectRandUserDesc": "Wählt einen Teilnehmer nach dem Zufallsprinzip aus",
"app.actionsBar.emojiMenu.statusTriggerLabel": "Status setzen",
"app.actionsBar.emojiMenu.awayLabel": "Abwesend",
"app.actionsBar.emojiMenu.awayDesc": "Ihren Status auf abwesend setzen",
@ -511,7 +511,7 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "Gruppenraum auswählen, dem beigetreten werden soll",
"app.breakoutTimeRemainingMessage": "Verbleibende Gruppenraumzeit: {0}",
"app.breakoutWillCloseMessage": "Zeit abgelaufen. Der Gruppenraum wird in Kürze geschlossen.",
"app.breakout.dropdown.manageDuration": "Dauer verwalten",
"app.breakout.dropdown.manageDuration": "Dauer ändern",
"app.breakout.dropdown.destroyAll": "Gruppenräume beenden",
"app.breakout.dropdown.options": "Gruppenraumoptionen",
"app.calculatingBreakoutTimeRemaining": "Berechne verbleibende Zeit...",
@ -576,13 +576,13 @@
"app.audio.listenOnly.closeLabel": "Schließen",
"app.audio.permissionsOverlay.title": "Zugriff auf das Mikrofon erlauben",
"app.audio.permissionsOverlay.hint": "Zugriff auf die Mediengeräte muss im Browser erlaubt werden, um an einer Audiokonferenz teilnehmen zu können.",
"app.error.removed": "Die Konferenz musste verlassen werden.",
"app.error.meeting.ended": "Konferenz wurde verlassen",
"app.meeting.logout.duplicateUserEjectReason": "Bereits anwesende:r Teilnehmer:in versucht erneut der Konferenz beizutreten",
"app.error.removed": "Sie wurden aus der Konferenz entfernt",
"app.error.meeting.ended": "Sie haben die Konferenz verlassen",
"app.meeting.logout.duplicateUserEjectReason": "Bereits anwesender Teilnehmer versucht erneut der Konferenz beizutreten",
"app.meeting.logout.permissionEjectReason": "Aufgrund einer Rechteverletzung aus der Konferenz entfernt worden",
"app.meeting.logout.ejectedFromMeeting": "Sie wurden aus der Konferenz entfernt",
"app.meeting.logout.validateTokenFailedEjectReason": "Verifikation des Autorisierungsmerkmals fehlgeschlagen",
"app.meeting.logout.userInactivityEjectReason": "Teilnehmer:in war zu lange inaktiv",
"app.meeting.logout.userInactivityEjectReason": "Teilnehmer war zu lange inaktiv",
"app.meeting-ended.rating.legendLabel": "Feedbackbewertung",
"app.meeting-ended.rating.starLabel": "Stern",
"app.modal.close": "Schließen",
@ -590,13 +590,13 @@
"app.modal.confirm": "Fertig",
"app.modal.newTab": "(Öffnet neuen Tab)",
"app.modal.confirm.description": "Änderungen speichern und Dialog schließen",
"app.modal.randomUser.noViewers.description": "Kein:e Teilnehmer:in zur zufälligen Auswahl verfügbar",
"app.modal.randomUser.selected.description": "zufällig ausgewählt",
"app.modal.randomUser.title": "Zufällig ausgewählte:r Teilnehmer:in",
"app.modal.randomUser.noViewers.description": "Keine Teilnehmer zur zufälligen Auswahl verfügbar",
"app.modal.randomUser.selected.description": "Sie wurden zufällig ausgewählt",
"app.modal.randomUser.title": "Zufällig ausgewählter Teilnehmer",
"app.modal.randomUser.who": "Wer wird ausgewählt...?",
"app.modal.randomUser.alone": "Es gibt nur eine:n Teilnehmer:in",
"app.modal.randomUser.alone": "Es gibt nur einen Teilnehmer",
"app.modal.randomUser.reselect.label": "Erneut auswählen",
"app.modal.randomUser.ariaLabel.title": "Dialog für zufällig ausgewählte:n Teilnehmer:in",
"app.modal.randomUser.ariaLabel.title": "Dialog für zufällig ausgewählten Teilnehmer",
"app.dropdown.close": "Schließen",
"app.dropdown.list.item.activeLabel": "Aktiv",
"app.error.400": "Ungültige Anfrage",
@ -606,9 +606,10 @@
"app.error.408": "Authentifizierung fehlgeschlagen",
"app.error.410": "Die Konferenz ist zu Ende",
"app.error.500": "Ups, irgendwas ist schiefgelaufen",
"app.error.userLoggedOut": "Teilnehmer:in hat einen ungültigen Konferenz-Token, weil er:sie sich ausgeloggt hat",
"app.error.ejectedUser": "Teilnehmer:in hat einen ungültigen Konferenz-Token, weil er:sie gesperrt wurde",
"app.error.userBanned": "Teilnehmer:in wurde gesperrt",
"app.error.userLoggedOut": "Teilnehmer hat einen ungültigen Konferenz-Token, weil er sich ausgeloggt hat",
"app.error.ejectedUser": "Teilnehmer hat einen ungültigen Konferenz-Token, weil er gesperrt wurde",
"app.error.joinedAnotherWindow": "Diese Konferenz scheint in einem anderen Browserfenster geöffnet zu sein.",
"app.error.userBanned": "Teilnehmer wurde gesperrt",
"app.error.leaveLabel": "Erneut einloggen",
"app.error.fallback.presentation.title": "Es ist ein Fehler aufgetreten",
"app.error.fallback.presentation.description": "Er wurde protokolliert. Bitte versuchen, die Seite neu zu laden.",
@ -617,30 +618,30 @@
"app.guest.errorSeeConsole": "Fehler: Weitere Details in der Konsole.",
"app.guest.noModeratorResponse": "Keine Antwort vom Moderator.",
"app.guest.noSessionToken": "Kein Konferenz-Token erhalten.",
"app.guest.windowTitle": "Wartebereich für Gäste",
"app.guest.windowTitle": "BigBlueButton - Wartebereich für Gäste",
"app.guest.missingToken": "Gast fehlt Konferenz-Token.",
"app.guest.missingSession": "Gast fehlt in der Konferenz.",
"app.guest.missingMeeting": "Konferenz existiert nicht.",
"app.guest.meetingEnded": "Konferenz beendet.",
"app.guest.guestWait": "Bitte warten Sie, bis ein Moderator Ihre Teilnahme an der Konferenz freigibt.",
"app.guest.guestDeny": "Moderator:in hat die Teilnahme an der Konferenz abgelehnt.",
"app.guest.guestDeny": "Der Moderator hat die Teilnahme an der Konferenz abgelehnt.",
"app.guest.seatWait": "Gast wartet auf die Teilnahme an der Konferenz.",
"app.guest.allow": "Gast zugelassen und zur Konferenz weitergeleitet.",
"app.guest.firstPositionInWaitingQueue": "Nun Erste:r in der Warteschlange!",
"app.guest.firstPositionInWaitingQueue": "Sie sind der Erste in der Warteschlange!",
"app.guest.positionInWaitingQueue": "Aktuelle Position in der Warteschlange: ",
"app.guest.guestInvalid": "Gastteilnehmer ist ungültig",
"app.guest.meetingForciblyEnded": "An einer Konferenz, die bereits beendet wurde, kann nicht teilgenommen werden",
"app.userList.guest.waitingUsers": "Wartende Teilnehmer:innen",
"app.userList.guest.waitingUsers": "Wartende Teilnehmer",
"app.userList.guest.waitingUsersTitle": "Teilnehmerverwaltung",
"app.userList.guest.optionTitle": "Unbearbeitete Teilnehmer:innen überprüfen",
"app.userList.guest.optionTitle": "Unbearbeitete Teilnehmer überprüfen",
"app.userList.guest.allowAllAuthenticated": "Alle Autorisierten erlauben",
"app.userList.guest.allowAllGuests": "Alle Gäste erlauben",
"app.userList.guest.allowEveryone": "Alle erlauben",
"app.userList.guest.denyEveryone": "Alle verweigern",
"app.userList.guest.pendingUsers": "{0} unbearbeitete Teilnehmer:innen",
"app.userList.guest.noPendingUsers": "Derzeit keine ausstehenden Teilnehmer:innen ...",
"app.userList.guest.pendingUsers": "{0} unbearbeitete Teilnehmer",
"app.userList.guest.noPendingUsers": "Derzeit keine ausstehenden Teilnehmer...",
"app.userList.guest.pendingGuestUsers": "{0} wartende Gäste",
"app.userList.guest.pendingGuestAlert": "Ist der Konferenz beigetreten und wartet auf Teilnahmeerlaubnis",
"app.userList.guest.pendingGuestAlert": "Ist der Konferenz beigetreten und wartet auf Ihre Teilnahmeerlaubnis",
"app.userList.guest.rememberChoice": "Auswahl für die Zukunft speichern",
"app.userList.guest.emptyMessage": "Momentan keine Nachricht vorhanden",
"app.userList.guest.inputPlaceholder": "Nachricht für den Wartebereich",
@ -655,7 +656,7 @@
"app.toast.chat.system": "System",
"app.toast.clearedEmoji.label": "Emojistatus zurückgesetzt",
"app.toast.setEmoji.label": "Emojistatus auf {0} gesetzt",
"app.toast.meetingMuteOn.label": "Alle Teilnehmer:innen wurden stummgeschaltet",
"app.toast.meetingMuteOn.label": "Alle Teilnehmer wurden stummgeschaltet",
"app.toast.meetingMuteOff.label": "Konferenz-Stummschaltung ausgeschaltet",
"app.toast.setEmoji.raiseHand": "Sie haben Ihre Hand gehoben",
"app.toast.setEmoji.lowerHand": "Habe meine Hand gesenkt",
@ -692,7 +693,7 @@
"app.shortcut-help.toggleFullscreenKey": "Enter",
"app.shortcut-help.nextSlideKey": "Pfeil rechts",
"app.shortcut-help.previousSlideKey": "Pfeil links",
"app.lock-viewers.title": "Rechte der Teilnehmer:innen einschränken",
"app.lock-viewers.title": "Teilnehmerrechte einschränken",
"app.lock-viewers.description": "Diese Optionen ermöglichen es, bestimmte Funktionen für Teilnehmer einzuschränken.",
"app.lock-viewers.featuresLable": "Funktion",
"app.lock-viewers.lockStatusLabel": "Status",
@ -786,7 +787,7 @@
"app.video.notReadableError": "Konnte nicht auf die Webcam zugreifen. Bitte prüfen, dass kein anderes Programm auf die Webcam zugreift",
"app.video.timeoutError": "Der Browser hat nicht rechtzeitig reagiert.",
"app.video.genericError": "Ein unbekannter Fehler ist mit der Kamera aufgetreten (Fehler {0})",
"app.video.mediaTimedOutError": "Die Webcam-Freigabe wurde unterbrochen. Bitte erneut starten.",
"app.video.mediaTimedOutError": "Ihre Webcam-Freigabe wurde unterbrochen. Bitte erneut starten.",
"app.video.mediaFlowTimeout1020": "Verbindung zum Server konnte nicht hergestellt werden (Fehler 1020)",
"app.video.suggestWebcamLock": "Sperrung der Teilnehmerwebcams aktivieren?",
"app.video.suggestWebcamLockReason": "(dies wird die Stabilität der Konferenz erhöhen)",
@ -810,7 +811,7 @@
"app.video.virtualBackground.background": "Hintergrund",
"app.video.virtualBackground.genericError": "Virtueller Hintergrund konnte nicht angewendet werden. Bitte erneut versuchen.",
"app.video.virtualBackground.camBgAriaDesc": "Setzt den virtuellen Hintergrund der Webcam auf {0}",
"app.video.camCapReached": "Es kann keine weitere Kamera freigegeben werden",
"app.video.camCapReached": "Sie können keine weiteren Kameras freigeben",
"app.video.meetingCamCapReached": "Konferenz hat die maximale Anzahl gleichzeitiger Kameras erreicht",
"app.video.dropZoneLabel": "Hier loslassen",
"app.fullscreenButton.label": "{0} als Vollbild darstellen",
@ -863,7 +864,7 @@
"app.whiteboard.toolbar.fontSize": "Schriftgrößenliste",
"app.whiteboard.toolbarAriaLabel": "Präsentationswerkzeuge",
"app.feedback.title": "Sie haben sich aus der Konferenz ausgeloggt",
"app.feedback.subtitle": "Wir würden gerne erfahren ob die Technik geklappt hat (optional)",
"app.feedback.subtitle": "Wir würden gerne erfahren, wie Sie BigBlueButton finden (optional)",
"app.feedback.textarea": "Wie können wir BigBlueButton verbessern?",
"app.feedback.sendFeedback": "Feedback senden",
"app.feedback.sendFeedbackDesc": "Feedback abschicken und Konferenz verlassen",
@ -873,12 +874,12 @@
"app.videoDock.webcamFocusDesc": "Ausgewählte Webcam vergrößern",
"app.videoDock.webcamUnfocusLabel": "Normalgröße",
"app.videoDock.webcamUnfocusDesc": "Ausgewählte Webcam auf Normalgröße verkleinern",
"app.videoDock.webcamPinLabel": "Kamera anheften",
"app.videoDock.webcamPinDesc": "Ausgewählte Webcam fixieren",
"app.videoDock.webcamUnpinLabel": "Kamera lösen",
"app.videoDock.webcamUnpinLabelDisabled": "Nur Moderator:innen können Teilnehmer:innen lösen",
"app.videoDock.webcamPinLabel": "Anheften",
"app.videoDock.webcamPinDesc": "Ausgewählte Webcam anheften",
"app.videoDock.webcamUnpinLabel": "Lösen",
"app.videoDock.webcamUnpinLabelDisabled": "Nur Moderatoren können Teilnehmer lösen",
"app.videoDock.webcamUnpinDesc": "Ausgewählte Webcam lösen",
"app.videoDock.autoplayBlockedDesc": "Wir benötigen Ihre/deine Zustimmung, um die Webcams anderer Teilnehmer:innen zu zeigen.",
"app.videoDock.autoplayBlockedDesc": "Wir benötigen Ihre Zustimmung, um Ihnen die Webcams anderer Teilnehmer zu zeigen.",
"app.videoDock.autoplayAllowLabel": "Webcams zeigen",
"app.invitation.title": "Gruppenraumeinladung",
"app.invitation.confirm": "Einladen",
@ -887,7 +888,7 @@
"app.createBreakoutRoom.breakoutRoomLabel": "Gruppenräume {0}",
"app.createBreakoutRoom.askToJoin": "Raum beitreten",
"app.createBreakoutRoom.generatingURL": "Erzeuge URL",
"app.createBreakoutRoom.generatingURLMessage": "Wir generieren eine Teilnahme-URL für den ausgewählten Gruppenraum. Das kann ein paar Sekunden dauern ...",
"app.createBreakoutRoom.generatingURLMessage": "Wir generieren eine Teilnahme-URL für den ausgewählten Gruppenraum. Das kann ein paar Sekunden dauern...",
"app.createBreakoutRoom.duration": "Dauer {0}",
"app.createBreakoutRoom.room": "Raum {0}",
"app.createBreakoutRoom.notAssigned": "Nicht zugewiesen ({0})",
@ -902,7 +903,7 @@
"app.createBreakoutRoom.randomlyAssign": "Zufällig zuordnen",
"app.createBreakoutRoom.randomlyAssignDesc": "Teilnehmer zufällig in Gruppenräume zuordnen",
"app.createBreakoutRoom.resetAssignments": "Zuordnungen zurücksetzen",
"app.createBreakoutRoom.resetAssignmentsDesc": "Alle Raumzuordnungen der Teilnehmer:innen zurücksetzen",
"app.createBreakoutRoom.resetAssignmentsDesc": "Alle Raumzuordnungen der Teilnehmer zurücksetzen",
"app.createBreakoutRoom.endAllBreakouts": "Alle Gruppenräume beenden",
"app.createBreakoutRoom.chatTitleMsgAllRooms": "alle Räume",
"app.createBreakoutRoom.msgToBreakoutsSent": "Nachricht wurde an {0} Gruppenräume gesendet",
@ -912,10 +913,10 @@
"app.createBreakoutRoom.minusRoomTime": "Gruppenraumzeit verringern auf",
"app.createBreakoutRoom.addRoomTime": "Gruppenraumzeit erhöhen auf",
"app.createBreakoutRoom.addParticipantLabel": "+ Teilnehmer hinzufügen",
"app.createBreakoutRoom.freeJoin": "Erlauben, sich selbst einen Gruppenraum auszusuchen.",
"app.createBreakoutRoom.leastOneWarnBreakout": "Jedem Gruppenraum muss wenigstens ein:e Teilnehmer:in zugeordnet sein.",
"app.createBreakoutRoom.freeJoin": "Den Teilnehmern erlauben, sich selbst einen Gruppenraum auszusuchen.",
"app.createBreakoutRoom.leastOneWarnBreakout": "Jedem Gruppenraum muss wenigstens ein Teilnehmer zugeordnet sein.",
"app.createBreakoutRoom.minimumDurationWarnBreakout": "Die Mindestdauer für einen Gruppenraum beträgt {0} Minuten.",
"app.createBreakoutRoom.modalDesc": "Tipp: Teilnehmer:in per Drag-and-Drop einem bestimmten Gruppenraum zuweisen.",
"app.createBreakoutRoom.modalDesc": "Tipp: Sie können die Teilnehmer per Drag-and-Drop einem bestimmten Gruppenraum zuweisen.",
"app.createBreakoutRoom.roomTime": "{0} Minuten",
"app.createBreakoutRoom.numberOfRoomsError": "Die Raumanzahl ist ungültig.",
"app.createBreakoutRoom.duplicatedRoomNameError": "Raumname kann nicht doppelt vergeben werden.",
@ -1036,7 +1037,7 @@
"app.learningDashboard.usersTable.userStatusOffline": "Offline",
"app.learningDashboard.usersTable.noUsers": "Bisher keine Teilnehmer",
"app.learningDashboard.usersTable.name": "Name",
"app.learningDashboard.usersTable.moderator": "Moderator:in",
"app.learningDashboard.usersTable.moderator": "Moderator",
"app.learningDashboard.usersTable.pollVotes": "Abstimmungen",
"app.learningDashboard.usersTable.join": "Teilnahme",
"app.learningDashboard.usersTable.left": "Verlassen",
@ -1052,11 +1053,11 @@
"app.learningDashboard.errors.dataUnavailable": "Die Daten sind nicht mehr verfügbar",
"mobileApp.portals.list.empty.addFirstPortal.label": "Fügen Sie Ihr erstes Portal über die Schaltfläche oben hinzu,",
"mobileApp.portals.list.empty.orUseOurDemoServer.label": "oder nutzen Sie unseren Demo-Server.",
"mobileApp.portals.list.add.button.label": "Neues Portal",
"mobileApp.portals.list.add.button.label": "Portal hinzufügen",
"mobileApp.portals.fields.name.label": "Portalname",
"mobileApp.portals.fields.name.placeholder": "BigBlueButton Demo",
"mobileApp.portals.fields.url.label": "Server-URL",
"mobileApp.portals.addPortalPopup.confirm.button.label": "Portal hinzufügen",
"mobileApp.portals.addPortalPopup.confirm.button.label": "Speichern",
"mobileApp.portals.drawerNavigation.button.label": "Portale",
"mobileApp.portals.addPortalPopup.validation.emptyFields": "Benötigte Felder",
"mobileApp.portals.addPortalPopup.validation.portalNameAlreadyExists": "Name existiert bereits",

View File

@ -485,7 +485,6 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "Επιλέξτε ένα υπο-δωμάτιο (breakout) για συμμετοχή",
"app.breakoutTimeRemainingMessage": "Χρόνος υπο-δωματίου (breakout) που απομένει: {0}",
"app.breakoutWillCloseMessage": "Ο χρόνος τελείωσε. Το υπο-δωμάτιο (breakout) θα κλείσει σύντομα.",
"app.breakout.dropdown.manageDuration": "Διαχείριση διάρκειας",
"app.calculatingBreakoutTimeRemaining": "Υπολογισμός υπόλοιπου χρόνου ...",
"app.audioModal.ariaTitle": "Συμμετοχή με τυπικό ήχο",
"app.audioModal.microphoneLabel": "Μικρόφωνο",

View File

@ -541,8 +541,8 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "Choose a breakout room to join",
"app.breakoutTimeRemainingMessage": "Breakout room time remaining: {0}",
"app.breakoutWillCloseMessage": "Time ended. Breakout room will close soon",
"app.breakout.dropdown.manageDuration": "Manage duration",
"app.breakout.dropdown.destroyAll": "Destroy breakouts",
"app.breakout.dropdown.manageDuration": "Change duration",
"app.breakout.dropdown.destroyAll": "End breakout rooms",
"app.breakout.dropdown.options": "Breakout Options",
"app.calculatingBreakoutTimeRemaining": "Calculating remaining time ...",
"app.audioModal.ariaTitle": "Join audio modal",
@ -643,6 +643,7 @@
"app.error.500": "Ops, something went wrong",
"app.error.userLoggedOut": "User has an invalid sessionToken due to log out",
"app.error.ejectedUser": "User has an invalid sessionToken due to ejection",
"app.error.joinedAnotherWindow": "This session seems to be opened in another browser window.",
"app.error.userBanned": "User has been banned",
"app.error.leaveLabel": "Log in again",
"app.error.fallback.presentation.title": "An error occurred",

View File

@ -510,8 +510,6 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "Vali eraldatud ruum, millega liituda",
"app.breakoutTimeRemainingMessage": "Eraldatud ruumi järelejäänud aeg: {0}",
"app.breakoutWillCloseMessage": "Aeg sai läbi. Eraldatud ruum suletakse kohe",
"app.breakout.dropdown.manageDuration": "Halda kestust",
"app.breakout.dropdown.destroyAll": "Lõpeta eraldatud ruumid",
"app.breakout.dropdown.options": "Eraldatud ruumide valikud",
"app.calculatingBreakoutTimeRemaining": "Arvutan järelejäänud aega...",
"app.audioModal.ariaTitle": "Audioga liitumise aken",

View File

@ -510,8 +510,6 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "Aukeratu azpitalde baten gela bertara sartzeko",
"app.breakoutTimeRemainingMessage": "Azpitaldearen gelari geratzen zaion denbora: {0}",
"app.breakoutWillCloseMessage": "Denbora agortu da. Azpitaldearen gela laster itxiko da",
"app.breakout.dropdown.manageDuration": "Kudeatu iraupena",
"app.breakout.dropdown.destroyAll": "Bukatu azpitaldeak",
"app.breakout.dropdown.options": "Azpitaldeen aukerak",
"app.calculatingBreakoutTimeRemaining": "Geratzen den denbora kalkulatzen...",
"app.audioModal.ariaTitle": "Erabili audio modala",

View File

@ -6,6 +6,7 @@
"app.chat.disconnected": "ارتباط شما قطع شده است، امکان ارسال پیام‌ها وجود ندارد",
"app.chat.locked": "گفنگو قفل شده است، امکان ارسال هیچ پیامی وجود ندارد",
"app.chat.inputLabel": "ورودی پیام برای گفتگو {0}",
"app.chat.inputPlaceholder": "پیام‌ها {0}",
"app.chat.titlePublic": "گفتگوی عمومی",
"app.chat.titlePrivate": "گفتگوی خصوصی با {0}",
"app.chat.partnerDisconnected": "{0} جلسه را ترک کرد",
@ -19,6 +20,7 @@
"app.chat.label": "گفتگو",
"app.chat.offline": "آفلاین",
"app.chat.pollResult": "نتایج نظرسنجی",
"app.chat.breakoutDurationUpdated": "زمان جلسه زیرمجموعه اکنون {0} دقیقه است. ",
"app.chat.emptyLogLabel": "پاک کردن سابقه گفتگو",
"app.chat.clearPublicChatMessage": "سابقه گفتگوها توسط مدیر حذف گردید",
"app.chat.multi.typing": "چند کاربر در حال نوشتن هستند",
@ -41,8 +43,23 @@
"app.captions.menu.backgroundColor": "رنگ پس‌زمینه",
"app.captions.menu.previewLabel": "پیش‌نمایش",
"app.captions.menu.cancelLabel": "لغو",
"app.captions.hide": "پنهان‌سازی زیرنویس‌ها",
"app.captions.ownership": "گرفتن کنترل",
"app.captions.ownershipTooltip": "شما به عنوان صاحب زیرنویس‌های {0} منسوب خواهید شد",
"app.captions.dictationStart": "آغاز نوشتن کلمات",
"app.captions.dictationStop": "توقف نوشتن کلمات",
"app.captions.dictationOnDesc": "روشن کردن امکان تشخیص گفتار",
"app.captions.dictationOffDesc": "غیرفعال کردن امکان تشخیص گفتار",
"app.captions.speech.start": "امکان تشخیص گفتار آغاز شد",
"app.captions.speech.stop": "امکان تشخیص گفتار متوقف شد",
"app.captions.speech.error": "به دلیل ناسازگاری مرورگر یا ایجاد وقفه، تشخیص گفتار متوقف شده است",
"app.textInput.sendLabel": "ارسال",
"app.title.defaultViewLabel": "نمای پیش‌فرض ارائه",
"app.notes.title": "یادداشت‌های اشتراکی",
"app.notes.label": "یادداشت‌ها",
"app.notes.hide": "پنهان کردن یادداشت‌ها",
"app.notes.locked": "قفل شده",
"app.pads.hint": "برای تمرکز روی نوار ابزار برگه یادداشت، Esc را فشار دهید",
"app.user.activityCheck": "بررسی فعالیت کاربر",
"app.user.activityCheck.label": "بررسی کنید آیا کاربر هنوز در جلسه ({0}) حضور دارد",
"app.user.activityCheck.check": "بررسی",
@ -101,6 +118,7 @@
"app.userList.userOptions.disableMic": "استفاده از امکان صدا برای کاربران غیرفعال شده است",
"app.userList.userOptions.disablePrivChat": "گفتگوی خصوصی غیرفعال شده است",
"app.userList.userOptions.disablePubChat": "گفتگوی عمومی غیرفعال شده است",
"app.userList.userOptions.disableNotes": "یادداشت اشتراکی در حال حاضر قفل هستند",
"app.userList.userOptions.hideUserList": "لیست کاربران در حال حاضر برای شرکت کنندگان قابل مشاهده نیست",
"app.userList.userOptions.webcamsOnlyForModerator": "تنها مدیران امکان مشاهده دوربین‌های کاربران را دارند (به دلیل تنظیمات قفل)",
"app.userList.content.participants.options.clearedStatus": "وضعیت همه کاربرها پاک شد",
@ -108,11 +126,14 @@
"app.userList.userOptions.enableMic": "میکروفون کاربران فعال شد",
"app.userList.userOptions.enablePrivChat": "گفتگوی خصوصی فعال شد",
"app.userList.userOptions.enablePubChat": "گفتگوی عمومی فعال شد",
"app.userList.userOptions.enableNotes": "یادداشت‌های اشتراکی در حال حاضر فعال هستند",
"app.userList.userOptions.showUserList": "لیست کاربران در حال حاضر برای شرکت کنندگان قابل مشاهده است",
"app.userList.userOptions.enableOnlyModeratorWebcam": "شما در حال حاضر می‌توانید دوربین خود را به اشتراک بگذارید، همه تصویر شما را خواهند دید",
"app.userList.userOptions.savedNames.title": "لیست کاربران در جلسه {0} در {1}",
"app.userList.userOptions.sortedFirstName.heading": "مرتب شده بر اساس نام:",
"app.userList.userOptions.sortedLastName.heading": "مرتب شده بر اساس نام خانوادگی:",
"app.userList.userOptions.hideViewersCursor": "نشانگر بیننده قفل شده است",
"app.userList.userOptions.showViewersCursor": "قفل نشانگر بیننده باز شده است",
"app.media.label": "صدا و تصویر",
"app.media.autoplayAlertDesc": "دادن اجازه دسترسی",
"app.media.screenshare.start": "اشتراک صفحه نمایش شروع شد",
@ -125,9 +146,9 @@
"app.screenshare.presenterLoadingLabel": "اشتراک‌گذاری صفحه نمایش شما در حال بارگذاری است",
"app.screenshare.viewerLoadingLabel": "اشتراک‌گذاری صفحه نمایش ارائه‌دهنده در حال بارگذاری است",
"app.screenshare.presenterSharingLabel": "اکنون شما در حال اشتراک‌گذاری صفحه نمایش‌تان هستید",
"app.screenshare.screenshareFinalError": "کد {0}: نتوانست صفحه‌ی نمایش را به اشتراک بگذارد.",
"app.screenshare.screenshareFinalError": "کد {0}: اشتراک‌گذاری صفحه‌ی نمایش امکان‌پذیر نیست..",
"app.screenshare.screenshareRetryError": "کد {0}: دوباره سعی کنید صفحه نمایش را به اشتراک بگذارید. ",
"app.screenshare.screenshareRetryOtherEnvError": "کد {0}: نتواست صفحه را به اشتراک بگذارد. با مرورگر یا وسیله دیگری امتحان کنید.",
"app.screenshare.screenshareRetryOtherEnvError": "کد {0}: اشتراک‌گذاری صفحه‌ی نمایش امکان‌پذیر نیست. با مرورگر یا وسیله دیگری امتحان کنید.",
"app.screenshare.screenshareUnsupportedEnv": "کد {0}: مرورگر پشتیبانی نمی شود. با مرورگر یا وسیله دیگری امتحان کنید.",
"app.screenshare.screensharePermissionError": "کد {0}: برای گرفتن صفحه باید دسترسی آن را فعال کنید.",
"app.meeting.ended": "جلسه پایان یافت",
@ -149,6 +170,13 @@
"app.presentation.endSlideContent": "انتهای محتوای اسلاید",
"app.presentation.changedSlideContent": "ارائه به اسلاید {0} تغییر کرد",
"app.presentation.emptySlideContent": "محتوایی برای اسلاید کنونی وجود ندارد",
"app.presentation.options.fullscreen": "تمام صفحه",
"app.presentation.options.exitFullscreen": "بستن حالت تمام صفحه",
"app.presentation.options.minimize": "کمینه‌کردن",
"app.presentation.options.snapshot": "تصویر لحظه‌ای نمای فعلی ارائه",
"app.presentation.options.downloading": "در حال بارگیری...",
"app.presentation.options.downloaded": "ارائه فعلی بارگیری شد",
"app.presentation.options.downloadFailed": "بارگیری ارائه فعلی امکان‌پذیر نیست",
"app.presentation.presentationToolbar.noNextSlideDesc": "انتهای ارائه",
"app.presentation.presentationToolbar.noPrevSlideDesc": "ابتدای ارائه",
"app.presentation.presentationToolbar.selectLabel": "انتخاب اسلاید",
@ -173,6 +201,7 @@
"app.presentation.presentationToolbar.fitToWidth": "اندازه تصویر را متناسب با عرض ارائه کن",
"app.presentation.presentationToolbar.fitToPage": "اندازه تصویر را متناسب با عرض صفحه کن",
"app.presentation.presentationToolbar.goToSlide": "اسلاید {0}",
"app.presentation.placeholder": "در حال حاضر هیچ ارائه فعالی وجود ندارد",
"app.presentationUploder.title": "ارائه",
"app.presentationUploder.message": "به عنوان یک ارائه دهنده شما قادرید انواع فایل های مجموعه آفیس و یا فایل PDF را بارگذاری نمایید؛ پیشنهاد ما برای رسیدن به بهترین نتایج، استفاده از فایل PDF میباشد. لطفا از انتخاب بودن یک ارائه توسط گزینه سمت راست اطمینان حاصل کنید.",
"app.presentationUploder.extraHint": "*مهم*: هر فایل نباید از {0} مگابایت و {1} صفحه تجاوز کند. ",
@ -188,6 +217,7 @@
"app.presentationUploder.fileToUpload": "آماده بارگذاری ...",
"app.presentationUploder.currentBadge": "کنونی",
"app.presentationUploder.rejectedError": "پرونده(های) انتخاب شده رد شدند. لطفا نوع پرونده(ها) را بررسی کنید.",
"app.presentationUploder.connectionClosedError": "به دلیل اتصال ضعیف قطع شد. لطفا دوباره تلاش کنید. ",
"app.presentationUploder.upload.progress": "در حال بارگذاری ({0}%)",
"app.presentationUploder.upload.413": "حجم پرونده زیاد است، از حداکثر {0} مگابایت بیشتر است",
"app.presentationUploder.genericError": "آخ، خطای پیش آمده است...",
@ -226,6 +256,7 @@
"app.poll.autoOptionInstructions.label": "انتخاب خودکار فعال است - سوال نظرسنجی و گزینه(های) را در قالب داده شده بنویسید.",
"app.poll.maxOptionsWarning.label": "فقط 5 گزینه اول قابل استفاده است!",
"app.poll.pollPaneTitle": "نظرسنجی",
"app.poll.enableMultipleResponseLabel": "اجازه برای پاسخ‌های متعدد به ازای هر پاسخ‌دهنده؟ ",
"app.poll.quickPollTitle": "نظرسنجی سریع",
"app.poll.hidePollDesc": "پنهان‌سازی منوی نظرسنجی",
"app.poll.quickPollInstruction": "برای شروع نظرسنجی خود، گزینه زیر را انتخاب کنید.",
@ -250,11 +281,11 @@
"app.poll.typedResponse.desc" : "برای پرکردن پاسخ خود، یک جعبه متن به کاربران نمایش داده می شود.",
"app.poll.addItem.label" : "اضافه کردن آیتم",
"app.poll.start.label" : "آغاز نظرسنجی",
"app.poll.secretPoll.label" : "رأی‌گیری ناشناس",
"app.poll.secretPoll.label" : "نظرسنجی ناشناس",
"app.poll.secretPoll.isSecretLabel": "این نظرسنجی ناشناس است - شما قادر نخواهید بود پاسخ‌های فردی را ببینید.",
"app.poll.questionErr": "ارائه یک سوال الزامی است.",
"app.poll.optionErr": "یک گزینه نظرسنجی وارد کنید",
"app.poll.startPollDesc": "آغاز رای‌گیری",
"app.poll.startPollDesc": "آغاز نظرسنجی",
"app.poll.showRespDesc": "نمایش پاسخ پیکربندی",
"app.poll.addRespDesc": "اضافه کردن ورودی پاسخ نظر سنجی",
"app.poll.deleteRespDesc": "حذف گزینه {0}",
@ -282,7 +313,7 @@
"app.poll.liveResult.usersTitle": "کاربران",
"app.poll.liveResult.responsesTitle": "پاسخ",
"app.poll.liveResult.secretLabel": "این یک نظرسنجی ناشناس است. پاسخ هر فرد نمایش داده نمی‌شود.",
"app.poll.removePollOpt": "گزینه نظرسنجی حذف شد {0}",
"app.poll.removePollOpt": "گزینه {0} نظرسنجی حذف شد",
"app.poll.emptyPollOpt": "خالی",
"app.polling.pollingTitle": "امکانات نظرسنجی",
"app.polling.pollQuestionTitle": "سوال نظرسنجی",
@ -336,6 +367,7 @@
"app.endMeeting.noLabel": "خیر",
"app.about.title": "درباره",
"app.about.version": "نسخه کاربر:",
"app.about.version_label": "نسخه بیگ‌بلو‌باتن:",
"app.about.copyright": "حق نشر:",
"app.about.confirmLabel": "تایید",
"app.about.confirmDesc": "تایید",
@ -396,6 +428,7 @@
"app.settings.dataSavingTab.description": "برای صرفه جویی در مصرف پهنای باند اینترنت آیتم هایی که باید نمایش داده شوند را انتخاب کنید.",
"app.settings.save-notification.label": "تنظیمات ذخیره شدند",
"app.statusNotifier.lowerHands": "دست‌های پایین",
"app.statusNotifier.lowerHandDescOneUser": "پایین آوردن دست {0}",
"app.statusNotifier.raisedHandsTitle": "دست‌های بالا برده شده",
"app.statusNotifier.raisedHandDesc": "{0} دستشان را بالا بردند",
"app.statusNotifier.raisedHandDescOneUser": "{0} دستش را بالا برد",
@ -477,6 +510,9 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "یک اتاق مجموعه را برای ملحق شدن انتخاب کنید",
"app.breakoutTimeRemainingMessage": "زمان باقی مانده از اتاق زیرمجموعه: {0}",
"app.breakoutWillCloseMessage": "زمان به اتمام رسید. اتاق زیرمجموعه به زودی بسته خواهد شد",
"app.breakout.dropdown.manageDuration": "تغیر مدت زمان",
"app.breakout.dropdown.destroyAll": "اتمام اتاق‌های زیرمجموعه",
"app.breakout.dropdown.options": "گزینه‌های زیرمجموعه",
"app.calculatingBreakoutTimeRemaining": "در حال محاسبه زمان باقی مانده ...",
"app.audioModal.ariaTitle": "ملحق شدن به مدال صدا",
"app.audioModal.microphoneLabel": "میکروفون",
@ -532,6 +568,7 @@
"app.audio.audioSettings.descriptionLabel": "لطفا توجه کنید، یک پیام در مرورگر شما ظاهر میشود، که از شما میخواهد اجازه اشتراک میکروفن خود را بدهید.",
"app.audio.audioSettings.microphoneSourceLabel": "منبع ورودی میکروفن",
"app.audio.audioSettings.speakerSourceLabel": "منبع خروجی صدا",
"app.audio.audioSettings.testSpeakerLabel": "بلندگوی خود را امتحان کنید",
"app.audio.audioSettings.microphoneStreamLabel": "بلندی صدای میکروفن شما",
"app.audio.audioSettings.retryLabel": "تلاش مجدد",
"app.audio.listenOnly.backLabel": "بازگشت",
@ -570,6 +607,7 @@
"app.error.500": "آخ، خطای پیش آمده است",
"app.error.userLoggedOut": "کاربر به خاطر خروج sessionToken غیر معتبر دارد",
"app.error.ejectedUser": "کاربر به خاطر اخراج sessionToken غیر معتبر دارد",
"app.error.joinedAnotherWindow": "به نظر می‌رسد که این جلسه در پنجره مرورگر دیگری باز شده است. ",
"app.error.userBanned": "کاربر مسدود شده است",
"app.error.leaveLabel": "دوباره وارد شوید",
"app.error.fallback.presentation.title": "یک خطا رخ داده است",
@ -588,6 +626,8 @@
"app.guest.guestDeny": "درخواست ورود به جلسه مورد پذیرش قرار نگرفت",
"app.guest.seatWait": "میهمان در انتظار اجازه ورود به اتاق می‌باشد.",
"app.guest.allow": "میربان تائید شد و به جلسه هدایت گردید.",
"app.guest.firstPositionInWaitingQueue": "شما اولین نفر در صف هستید!",
"app.guest.positionInWaitingQueue": "موقعیت فعلی شما در صف انتظار:",
"app.guest.guestInvalid": " کاربر مهمان نامعتبر است",
"app.guest.meetingForciblyEnded": "شما نمی‌توانید به جلسه‌ای بپیوندید که از قبل به اجبار پایان یافته است",
"app.userList.guest.waitingUsers": "کاربران در حال انتظار",
@ -598,11 +638,14 @@
"app.userList.guest.allowEveryone": "اجازه ورود به همه",
"app.userList.guest.denyEveryone": "رد همه",
"app.userList.guest.pendingUsers": "{0} کاربر در انتظار تایید",
"app.userList.guest.noPendingUsers": "در حال حاظر هیچ کاربر معلقی وجود ندارد...",
"app.userList.guest.pendingGuestUsers": "{0} کاربر مهمان در انتظار تایید",
"app.userList.guest.pendingGuestAlert": "به جلسه ملحق شد و منتظر تایید شماست",
"app.userList.guest.rememberChoice": "به یاد داشتن انتخاب",
"app.userList.guest.emptyMessage": "فعلا پیامی نیست",
"app.userList.guest.inputPlaceholder": "ارسال پیام به اتاق انتظار مهمانان",
"app.userList.guest.privateInputPlaceholder": "پیام به {0}",
"app.userList.guest.privateMessageLabel": "پیام‌دادن",
"app.userList.guest.acceptLabel": "پذیرفتن",
"app.userList.guest.denyLabel": "رد کردن",
"app.user-info.title": "جستجوی دایرکتوری",
@ -615,6 +658,9 @@
"app.toast.meetingMuteOn.label": "صدای همه کاربران به حالت بی‌صدا تغییر کرد",
"app.toast.meetingMuteOff.label": "امکان بی‌صدا کردن جلسه غیرفعال شد",
"app.toast.setEmoji.raiseHand": "شما دستتان را بالا برده اید",
"app.toast.setEmoji.lowerHand": "دست شما پایین آورده شد",
"app.toast.promotedLabel": "نقش شما به مدیر ارتقا یافت",
"app.toast.demotedLabel": "نقش شما به کاربر عادی تنزل یافت",
"app.notification.recordingStart": "جلسه در حال ضبط شدن است",
"app.notification.recordingStop": "این جلسه ضبط نمی‌شود",
"app.notification.recordingPaused": "جلسه دیگر ضبط نمی‌شود",
@ -642,6 +688,10 @@
"app.shortcut-help.toggleFullscreen": "تغییر حالت تمام صفحه (ارائه دهنده)",
"app.shortcut-help.nextSlideDesc": "اسلاید بعدی (ارائه دهنده)",
"app.shortcut-help.previousSlideDesc": "اسلاید قبلی (ارائه دهنده)",
"app.shortcut-help.togglePanKey": "کلید فاصله",
"app.shortcut-help.toggleFullscreenKey": "کلید Enter",
"app.shortcut-help.nextSlideKey": "کلید سمت راست",
"app.shortcut-help.previousSlideKey": "کلید سمت چپ",
"app.lock-viewers.title": "قفل کردن کاربران",
"app.lock-viewers.description": "این قابلیت شما را قادر می‌سازد تا دسترسی به امکانات ویژه را از کاربران بگیرید.",
"app.lock-viewers.featuresLable": "امکان",
@ -657,6 +707,7 @@
"app.lock-viewers.button.apply": "اعمال",
"app.lock-viewers.button.cancel": "لغو",
"app.lock-viewers.locked": "قفل شده",
"app.lock-viewers.hideViewersCursor": "مشاهده نشانگر دیگر کاربران",
"app.guest-policy.ariaTitle": "فرم تنظیمات سیاست پذیرش درخواست ورود کاربران",
"app.guest-policy.title": "سیاست پذیرش درخواست ورود کاربران",
"app.guest-policy.description": "سیاست پذیرش درخواست ورود کاربران در جلسه را مشخصی کنید",
@ -669,15 +720,26 @@
"app.connection-status.description": "وضعیت اتصال کاربران را مشاهده کنید",
"app.connection-status.empty": "در حال حاضر هیچ مشکلی در رابطه با اتصال گزارش نشده است",
"app.connection-status.more": "بیشتر",
"app.connection-status.copy": "کپی آمار",
"app.connection-status.copied": "کپی شد!",
"app.connection-status.jitter": "Jitter",
"app.connection-status.label": "وضعیت اتصال",
"app.connection-status.settings": "تغییر‌دادن تنظیمات شما",
"app.connection-status.no": "No",
"app.connection-status.notification": "قطعی در اتصال شما پیدا شد",
"app.connection-status.offline": "آفلاین",
"app.connection-status.audioUploadRate": "نرخ بارگذاری صدا",
"app.connection-status.audioDownloadRate": "نرخ بارگیری صدا",
"app.connection-status.videoUploadRate": "نرخ بارگذاری تصویر",
"app.connection-status.videoDownloadRate": "نرخ بارگیری تصویر",
"app.connection-status.lostPackets": "Lost packets",
"app.connection-status.usingTurn": "Using TURN",
"app.connection-status.yes": "Yes",
"app.connection-status.connectionStats": "آمار اتصال",
"app.connection-status.myLogs": "گزارش‌های من",
"app.connection-status.sessionLogs": "گزارش‌های جلسه",
"app.connection-status.next": "صفحه بعدی",
"app.connection-status.prev": "صفحه قبلی",
"app.learning-dashboard.label": "پیشخوان تحلیل یادگیری",
"app.learning-dashboard.description": "باز کردن پیشخوان به همراه فعالیت‌های کاربران",
"app.learning-dashboard.clickHereToOpen": "باز کردن پیشخوان تحلیل یادگیری",
@ -748,6 +810,8 @@
"app.video.virtualBackground.background": "پس‌زمینه",
"app.video.virtualBackground.genericError": "افکت دوربین اعمال نشد. مجددا تلاش کنید.",
"app.video.virtualBackground.camBgAriaDesc": "تنظیم پس‌زمینه مجازی دوربین به {0}",
"app.video.camCapReached": "نمی‌توانید دوربین‌های بیشتری را به اشتراک بگذارید",
"app.video.meetingCamCapReached": "جلسه به حد مجاز دوربین‌های همزمان خود رسیده است",
"app.video.dropZoneLabel": "اینجا بیندازید",
"app.fullscreenButton.label": "تغییر {0} به تمام صفحه",
"app.fullscreenUndoButton.label": "{0} تمام صفحه را واگرد کنید",
@ -837,7 +901,11 @@
"app.createBreakoutRoom.durationInMinutes": "مدت زمان (دقیقه)",
"app.createBreakoutRoom.randomlyAssign": "به صورت تصادفی واگذار شده",
"app.createBreakoutRoom.randomlyAssignDesc": "توزیع تصادفی کاربران به اتاق‌های زیر مجموعه",
"app.createBreakoutRoom.resetAssignments": "بازنشانی وظایف",
"app.createBreakoutRoom.resetAssignmentsDesc": "بازنشانی همه وظایف اتاق کاربر",
"app.createBreakoutRoom.endAllBreakouts": "پایان تمام اتاق‌های زیرمجموعه",
"app.createBreakoutRoom.chatTitleMsgAllRooms": "همه اتاق‌ها",
"app.createBreakoutRoom.msgToBreakoutsSent": "پیام به {0} اتاق زیرمجموعه ارسال شد",
"app.createBreakoutRoom.roomName": "{0} (اتاق - {1})",
"app.createBreakoutRoom.doneLabel": "انجام شد",
"app.createBreakoutRoom.nextLabel": "بعدی",
@ -852,6 +920,10 @@
"app.createBreakoutRoom.numberOfRoomsError": "تعداد اتاق ها نادرست است.",
"app.createBreakoutRoom.duplicatedRoomNameError": "نام اتاق نمی‌تواند تکراری باشد.",
"app.createBreakoutRoom.emptyRoomNameError": "نام اتاق نمی‌تواند خالی باشد.",
"app.createBreakoutRoom.setTimeInMinutes": "تنظیم مدت زمان به (دقیقه)",
"app.createBreakoutRoom.setTimeLabel": "اعمال",
"app.createBreakoutRoom.setTimeCancel": "لغو",
"app.createBreakoutRoom.setTimeHigherThanMeetingTimeError": "مدت زمان اتاق‌های زیرمجموعه نمی‌تواند از زمان جلسه بیشتر باشد.",
"app.createBreakoutRoom.roomNameInputDesc": "به روز رسانی نام اتاق‌های زیر مجموعه",
"app.externalVideo.start": "به اشتراک گذاری ویدئو جدید",
"app.externalVideo.title": "اشتراک یک ویدیوی خارجی",
@ -863,6 +935,8 @@
"app.externalVideo.refreshLabel": "تازه سازی پخش کننده ویدئو/صوت",
"app.externalVideo.fullscreenLabel": "پخش کننده ویدئو/صوت",
"app.externalVideo.noteLabel": "نکته: ویدیوهای خارجی به اشتراک گذاشته شده در ضبط ظاهر نمی‌شوند.نشانی‌های وب یوتیوب، ویمیو، Instructure Media، توییچ، دیلی‌موشن و فایل‌های رسانه‌ای (به عنوان مثال https://example.com/xy.mp4) پشتیبانی می‌شوند.",
"app.externalVideo.subtitlesOn": "خاموش‌کردن",
"app.externalVideo.subtitlesOff": "روشن‌کردن (در صورت وجود)",
"app.actionsBar.actionsDropdown.shareExternalVideo": "اشتراک یک ویدیوی خارجی",
"app.actionsBar.actionsDropdown.stopShareExternalVideo": "متوقف کردن نمایش ویدیوی خارجی",
"app.iOSWarning.label": "لطفا به iOS نسخه 12.2 یا بالاتر ارتقا دهید",
@ -892,6 +966,7 @@
"playback.button.search.aria": "جستجو",
"playback.button.section.aria": "بخش کناری",
"playback.button.swap.aria": "تعویض محتوا",
"playback.button.theme.aria": "تغییر زمینه",
"playback.error.wrapper.aria": "محدوده خطا",
"playback.loader.wrapper.aria": "محدوده بارگذارنده",
"playback.player.wrapper.aria": "محدوده پخش‌کننده",
@ -924,14 +999,30 @@
"playback.player.thumbnails.wrapper.aria": "محدوده تصاویر بندانگشتی",
"playback.player.webcams.wrapper.aria": "ناحیه وب‌کم",
"app.learningDashboard.dashboardTitle": "پیشخوان تحلیل یادگیری",
"app.learningDashboard.downloadSessionDataLabel": "بارگیری داده‌های جلسه",
"app.learningDashboard.lastUpdatedLabel": "آخرین به‌روز‌رسانی در",
"app.learningDashboard.sessionDataDownloadedLabel": "بارگیری شد!",
"app.learningDashboard.shareButton": "اشتراک‌گذاری با دیگران",
"app.learningDashboard.shareLinkCopied": "پیوند با موفقیت کپی شد!",
"app.learningDashboard.user": "کاربر",
"app.learningDashboard.indicators.meetingStatusEnded": "پایان یافته",
"app.learningDashboard.indicators.meetingStatusActive": "فعال",
"app.learningDashboard.indicators.usersOnline": "کاربران فعال",
"app.learningDashboard.indicators.usersTotal": "تعداد کل کاربران",
"app.learningDashboard.indicators.polls": "نظرسنجی‌ها",
"app.learningDashboard.indicators.timeline": "خط زمانی",
"app.learningDashboard.indicators.activityScore": "امتیاز فعالیت",
"app.learningDashboard.indicators.duration": "مدت زمان",
"app.learningDashboard.userDetails.startTime": "زمان شروع",
"app.learningDashboard.userDetails.endTime": "زمان پایان",
"app.learningDashboard.userDetails.joined": "پیوست",
"app.learningDashboard.userDetails.category": "دسته‌بندی",
"app.learningDashboard.userDetails.average": "میانگین",
"app.learningDashboard.userDetails.activityPoints": "نقاط فعالیت",
"app.learningDashboard.userDetails.poll": "نظرسنجی",
"app.learningDashboard.userDetails.response": "پاسخ",
"app.learningDashboard.userDetails.mostCommonAnswer": "رایج‌ترین پاسخ",
"app.learningDashboard.userDetails.anonymousAnswer": "نظرسنجی ناشناس",
"app.learningDashboard.usersTable.title": "مرور کلی",
"app.learningDashboard.usersTable.colOnline": "زمان آنلاین بودن",
"app.learningDashboard.usersTable.colTalk": "زمان صحبت کردن",
@ -944,10 +1035,32 @@
"app.learningDashboard.usersTable.userStatusOnline": "آنلاین",
"app.learningDashboard.usersTable.userStatusOffline": "آفلاین",
"app.learningDashboard.usersTable.noUsers": "هنوز هیچ کاربری وجود ندارد",
"app.learningDashboard.usersTable.name": "نام",
"app.learningDashboard.usersTable.moderator": "مدیر",
"app.learningDashboard.usersTable.pollVotes": "رأی‌های نظرسنجی",
"app.learningDashboard.usersTable.join": "پیوستن",
"app.learningDashboard.usersTable.left": "ترک‌کردن",
"app.learningDashboard.usersTable.notAvailable": "در دسترس نیست",
"app.learningDashboard.pollsTable.title": "نظرسنجی‌ها",
"app.learningDashboard.pollsTable.anonymousAnswer": "نظرسنجی ناشناس (پاسخ‌ها در ردیف آخر)",
"app.learningDashboard.pollsTable.anonymousRowName": "ناشناس",
"app.learningDashboard.pollsTable.noPollsCreatedHeading": "هیچ نظرسنجی ایجاد نشده است",
"app.learningDashboard.pollsTable.noPollsCreatedMessage": "زمانی که یک نظرسنجی برای کاربران ارسال شد، نتایج آن‌ها در این فهرست ظاهر می‌شود.",
"app.learningDashboard.statusTimelineTable.title": "خط زمانی",
"app.learningDashboard.statusTimelineTable.thumbnail": "تصویر کوچک ارائه",
"app.learningDashboard.errors.invalidToken": "توکن نشست نامعتبر است",
"app.learningDashboard.errors.dataUnavailable": "داده دیگر موجود نیست"
"app.learningDashboard.errors.dataUnavailable": "داده دیگر موجود نیست",
"mobileApp.portals.list.empty.addFirstPortal.label": "با استفاده از دکمه بالا، اولین درگاه خود را اضافه کنید،",
"mobileApp.portals.list.empty.orUseOurDemoServer.label": "یا از سرور‌های آزمایشی ما استفاده کنید.",
"mobileApp.portals.list.add.button.label": "افزودن درگاه",
"mobileApp.portals.fields.name.label": "نام درگاه",
"mobileApp.portals.fields.name.placeholder": "نسخه آزمایشی بیگ‌بلو‌باتن",
"mobileApp.portals.fields.url.label": "آدرس سرور",
"mobileApp.portals.addPortalPopup.confirm.button.label": "ذخیره",
"mobileApp.portals.drawerNavigation.button.label": "درگاه‌ها",
"mobileApp.portals.addPortalPopup.validation.emptyFields": "زمینه‌های مورد نیاز",
"mobileApp.portals.addPortalPopup.validation.portalNameAlreadyExists": "نام در حال حاضر استفاده شده است",
"mobileApp.portals.addPortalPopup.validation.urlInvalid": "خطا در تلاش برای بارگیری صفحه - آدرس و اتصال شبکه را بررسی کنید"
}

View File

@ -132,6 +132,8 @@
"app.userList.userOptions.savedNames.title": "Liste des utilisateurs de la conférence {0} à {1}",
"app.userList.userOptions.sortedFirstName.heading": "Trié par prénom :",
"app.userList.userOptions.sortedLastName.heading": "Trié par nom :",
"app.userList.userOptions.hideViewersCursor": "Les curseurs du visualiseur sont bloqués",
"app.userList.userOptions.showViewersCursor": "Les curseurs du visualiseur sont débloqués",
"app.media.label": "Média",
"app.media.autoplayAlertDesc": "Autoriser l'accès",
"app.media.screenshare.start": "Partage d'écran commencé",
@ -508,9 +510,9 @@
"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.breakout.dropdown.manageDuration": "Gérer la durée",
"app.breakout.dropdown.destroyAll": "Supprimer les salles attenantes",
"app.breakout.dropdown.options": "Options des salles attenantes",
"app.breakout.dropdown.manageDuration": "Changer la durée",
"app.breakout.dropdown.destroyAll": "Terminer les réunions privées",
"app.breakout.dropdown.options": "Options des réunions privées",
"app.calculatingBreakoutTimeRemaining": "Calcul du temps restant...",
"app.audioModal.ariaTitle": "Fenêtre modale pour joindre la réunion en audio",
"app.audioModal.microphoneLabel": "Microphone",
@ -566,6 +568,7 @@
"app.audio.audioSettings.descriptionLabel": "Veuillez noter qu'une boîte de dialogue apparaîtra dans votre navigateur, vous demandant d'accepter le partage de votre micro.",
"app.audio.audioSettings.microphoneSourceLabel": "Choix du micro",
"app.audio.audioSettings.speakerSourceLabel": "Choix du haut-parleur",
"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.listenOnly.backLabel": "Retour",
@ -604,6 +607,7 @@
"app.error.500": "Oups, quelque chose s'est mal passé",
"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",
"app.error.userBanned": "L'utilisateur a été banni",
"app.error.leaveLabel": "Connectez-vous à nouveau",
"app.error.fallback.presentation.title": "Une erreur s'est produite",
@ -684,6 +688,10 @@
"app.shortcut-help.toggleFullscreen": "Basculer le mode plein-écran (Présentateur)",
"app.shortcut-help.nextSlideDesc": "Diapositive suivante (présentateur)",
"app.shortcut-help.previousSlideDesc": "Diapositive précédente (présentateur)",
"app.shortcut-help.togglePanKey": "Barre d'espace",
"app.shortcut-help.toggleFullscreenKey": "Entrée",
"app.shortcut-help.nextSlideKey": "Flèche droite",
"app.shortcut-help.previousSlideKey": "Flèche gauche",
"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é",
@ -699,6 +707,7 @@
"app.lock-viewers.button.apply": "Appliquer",
"app.lock-viewers.button.cancel": "Annuler",
"app.lock-viewers.locked": "Verrouillé",
"app.lock-viewers.hideViewersCursor": "Voir les curseurs des autres spectateurs",
"app.guest-policy.ariaTitle": "Fenêtre des paramètres de gestion des accès",
"app.guest-policy.title": "Gestion des accès",
"app.guest-policy.description": "Modifier le paramétrage de la gestion des accès à la réunion ",
@ -926,6 +935,8 @@
"app.externalVideo.refreshLabel": "Réactualiser le lecteur vidéo",
"app.externalVideo.fullscreenLabel": "Lecteur vidéo",
"app.externalVideo.noteLabel": "Remarque : les vidéos externes partagées n'apparaîtront pas dans l'enregistrement. Les URL YouTube, Vimeo, Instructure Media, Twitch, Dailymotion et les URL de fichiers multimédias (par exemple https://example.com/xy.mp4) sont pris en charge.",
"app.externalVideo.subtitlesOn": "Éteindre",
"app.externalVideo.subtitlesOff": "Activer (si disponible)",
"app.actionsBar.actionsDropdown.shareExternalVideo": "Partager une vidéo externe",
"app.actionsBar.actionsDropdown.stopShareExternalVideo": "Arrêter le partage de vidéo externe",
"app.iOSWarning.label": "Veuillez mettre à jour vers iOS 12.2 ou supérieur",
@ -955,6 +966,7 @@
"playback.button.search.aria": "Rechercher",
"playback.button.section.aria": "Section de côté",
"playback.button.swap.aria": "Permuter le contenu",
"playback.button.theme.aria": "Basculer le thème",
"playback.error.wrapper.aria": "Zone d'erreur",
"playback.loader.wrapper.aria": "Zone de chargement",
"playback.player.wrapper.aria": "Zone de lecture",
@ -1038,10 +1050,17 @@
"app.learningDashboard.statusTimelineTable.thumbnail": "Vignette de présentation",
"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,",
"mobileApp.portals.list.empty.orUseOurDemoServer.label": "ou utilisez notre serveur de démo.",
"mobileApp.portals.list.add.button.label": "Ajouter un portail",
"mobileApp.portals.fields.name.label": "Nom du portail",
"mobileApp.portals.fields.name.placeholder": "Démo BigBlueButton",
"mobileApp.portals.fields.url.label": "URL du serveur",
"mobileApp.portals.drawerNavigation.button.label": "Portails"
"mobileApp.portals.addPortalPopup.confirm.button.label": "Enregistrer",
"mobileApp.portals.drawerNavigation.button.label": "Portails",
"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

@ -510,8 +510,8 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "Seleccionar a sala parcial na que incorporarse",
"app.breakoutTimeRemainingMessage": "Tempo restante da sala parcial: {0}",
"app.breakoutWillCloseMessage": "Rematou o tempo. A sala parcial pecharase en breve.",
"app.breakout.dropdown.manageDuration": "Xestionar a duración",
"app.breakout.dropdown.destroyAll": "Destruír as salas parciais",
"app.breakout.dropdown.manageDuration": "Cambiar a duración",
"app.breakout.dropdown.destroyAll": "Remate das salas parciais",
"app.breakout.dropdown.options": "Opcións das salas parciais",
"app.calculatingBreakoutTimeRemaining": "Calculando tempo restante…",
"app.audioModal.ariaTitle": " Xanela modal para unirse ao son",
@ -607,6 +607,7 @@
"app.error.500": "Ouh! algo foi mal",
"app.error.userLoggedOut": "O usuario ten un testemuño de sesión non válido por mor do peche da sesión",
"app.error.ejectedUser": "O usuario ten un testemuño de sesión non válido por mor da expulsión",
"app.error.joinedAnotherWindow": "Semella que esta sesión está aberta noutra xanela do navegador.",
"app.error.userBanned": "O usuario foi expulsado",
"app.error.leaveLabel": "Acceder de novo",
"app.error.fallback.presentation.title": "Produciuse un erro",

View File

@ -510,8 +510,6 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "Csoportterem választása",
"app.breakoutTimeRemainingMessage": "Csoport hátralévő ideje: {0}",
"app.breakoutWillCloseMessage": "Az idő lejárt. A csoportterem hamarosan bezárul",
"app.breakout.dropdown.manageDuration": "Időtartam kezelése",
"app.breakout.dropdown.destroyAll": "Csoporttermek megszüntetése",
"app.breakout.dropdown.options": "Csoporttermek beállításai",
"app.calculatingBreakoutTimeRemaining": "Hátralévő idő számítása ...",
"app.audioModal.ariaTitle": "Csatlakozás hangablak",

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