diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ClearPublicChatHistoryPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ClearPublicChatHistoryPubMsgHdlr.scala index 5da27cbac0..6c4c4f388a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ClearPublicChatHistoryPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ClearPublicChatHistoryPubMsgHdlr.scala @@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.chat import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.core.apps.{ ChatModel, PermissionCheck, RightsManagementTrait } import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.db.ChatMessageDAO import org.bigbluebutton.core.running.{ LiveMeeting, LogHelper } import org.bigbluebutton.core.domain.MeetingState2x @@ -31,6 +32,8 @@ trait ClearPublicChatHistoryPubMsgHdlr extends LogHelper with RightsManagementTr val newState = for { gc <- state.groupChats.find(msg.body.chatId) } yield { + ChatMessageDAO.deleteAllFromChat(liveMeeting.props.meetingProp.intId, msg.body.chatId) + ChatMessageDAO.insertSystemMsg(liveMeeting.props.meetingProp.intId, msg.body.chatId, "", "publicChatHistoryCleared", Map(), "") broadcastEvent(msg) val newGc = gc.clearMessages() val gcs = state.groupChats.update(newGc) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/ChatMessageDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/ChatMessageDAO.scala index 6e9b8f8ca4..bf9b4ecfdb 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/ChatMessageDAO.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/ChatMessageDAO.scala @@ -99,4 +99,16 @@ object ChatMessageDAO { } } + def deleteAllFromChat(meetingId: String, chatId: String) = { + DatabaseConnection.db.run( + TableQuery[ChatMessageDbTableDef] + .filter(_.meetingId === meetingId) + .filter(_.chatId === chatId) + .delete + ).onComplete { + case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) deleted from ChatMessage meetingId=${meetingId} chatId=${chatId}") + case Failure(e) => DatabaseConnection.logger.error(s"Error deleting from ChatMessage meetingId=${meetingId} chatId=${chatId}: $e") + } + } + } \ No newline at end of file diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/eventHandlers.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/eventHandlers.js index 912afcac7c..5e205e8719 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/eventHandlers.js @@ -5,8 +5,8 @@ import handleUserTyping from './handlers/userTyping'; import handleSyncGroupChatMsg from './handlers/syncGroupsChat'; import { processForHTML5ServerOnly } from '/imports/api/common/server/helpers'; -RedisPubSub.on('GetGroupChatMsgsRespMsg', processForHTML5ServerOnly(handleSyncGroupChatMsg)); -RedisPubSub.on('GroupChatMessageBroadcastEvtMsg', handleGroupChatMsgBroadcast); -RedisPubSub.on('ClearPublicChatHistoryEvtMsg', handleClearPublicGroupChat); -RedisPubSub.on('SyncGetGroupChatMsgsRespMsg', handleSyncGroupChatMsg); -RedisPubSub.on('UserTypingEvtMsg', handleUserTyping); +// RedisPubSub.on('GetGroupChatMsgsRespMsg', processForHTML5ServerOnly(handleSyncGroupChatMsg)); +// RedisPubSub.on('GroupChatMessageBroadcastEvtMsg', handleGroupChatMsgBroadcast); +// RedisPubSub.on('ClearPublicChatHistoryEvtMsg', handleClearPublicGroupChat); +// RedisPubSub.on('SyncGetGroupChatMsgsRespMsg', handleSyncGroupChatMsg); +// RedisPubSub.on('UserTypingEvtMsg', handleUserTyping); diff --git a/bigbluebutton-html5/imports/api/group-chat/server/eventHandlers.js b/bigbluebutton-html5/imports/api/group-chat/server/eventHandlers.js index c676a06732..4b699f1ca0 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/eventHandlers.js @@ -4,7 +4,7 @@ import handleGroupChatCreated from './handlers/groupChatCreated'; import handleGroupChatDestroyed from './handlers/groupChatDestroyed'; import { processForHTML5ServerOnly } from '/imports/api/common/server/helpers'; -RedisPubSub.on('GetGroupChatsRespMsg', processForHTML5ServerOnly(handleGroupChats)); -RedisPubSub.on('GroupChatCreatedEvtMsg', handleGroupChatCreated); -RedisPubSub.on('GroupChatDestroyedEvtMsg', handleGroupChatDestroyed); -RedisPubSub.on('SyncGetGroupChatsRespMsg', handleGroupChats); +// RedisPubSub.on('GetGroupChatsRespMsg', processForHTML5ServerOnly(handleGroupChats)); +// RedisPubSub.on('GroupChatCreatedEvtMsg', handleGroupChatCreated); +// RedisPubSub.on('GroupChatDestroyedEvtMsg', handleGroupChatDestroyed); +// RedisPubSub.on('SyncGetGroupChatsRespMsg', handleGroupChats); diff --git a/bigbluebutton-html5/imports/ui/Types/chat.ts b/bigbluebutton-html5/imports/ui/Types/chat.ts index 5292c27a6f..5de7344ce5 100644 --- a/bigbluebutton-html5/imports/ui/Types/chat.ts +++ b/bigbluebutton-html5/imports/ui/Types/chat.ts @@ -8,4 +8,5 @@ export interface Chat { totalUnread: number; userId: string; participant?: User; + lastSeenAt: number; } \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/Types/message.ts b/bigbluebutton-html5/imports/ui/Types/message.ts index 9b534cf33e..0ac80b2ede 100644 --- a/bigbluebutton-html5/imports/ui/Types/message.ts +++ b/bigbluebutton-html5/imports/ui/Types/message.ts @@ -8,9 +8,11 @@ export interface Message { createdTimeAsDate: string; meetingId: string; message: string; + messageType: string; messageId: string; senderId: string; senderName: string; senderRole: string; + messageMetadata: string; user: User; -} \ No newline at end of file +} diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/component.tsx index a44820d739..8bd4e6b198 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/component.tsx @@ -9,6 +9,8 @@ import { uid } from 'radash'; import Button from '/imports/ui/components/common/button/component'; import { clearPublicChatHistory, generateExportedMessages } from './services' import { getDateString } from '/imports/utils/string-utils'; +import Events from '/imports/ui/core/events/events'; +import { isEmpty } from 'ramda'; const CHAT_CONFIG = Meteor.settings.public.chat; const ENABLE_SAVE_AND_COPY_PUBLIC_CHAT = CHAT_CONFIG.enableSaveAndCopyPublicChat; @@ -38,15 +40,20 @@ const intlMessages = defineMessages({ id: 'app.chat.dropdown.options', description: 'Chat Options', }, + showWelcomeMessage: { + id: 'app.chat.dropdown.showWelcomeMessage', + description: 'Restore button label', + }, }); export const ChatActions: React.FC = () => { const intl = useIntl(); const isRTL = layoutSelect((i: Layout) => i.isRTL); - const uniqueIdsRef = useRef([uid(1), uid(2), uid(3)]); + const uniqueIdsRef = useRef([uid(1), uid(2), uid(3), uid(4)]); const downloadOrCopyRef = useRef<'download' | 'copy' | null>(null); const [userIsModerator, setUserIsmoderator] = useState(false); const [meetingIsBreakout, setMeetingIsBreakout] = useState(false); + const [showShowWelcomeMessages, setShowShowWelcomeMessages] = useState(false); const [ getChatMessageHistory, { @@ -65,7 +72,6 @@ export const ChatActions: React.FC = () => { useEffect(() => { if (dataHistory) { - console.log('dataHistory', dataHistory); const exportedString = generateExportedMessages( dataHistory.chat_message_public, dataHistory.user_welcomeMsgs[0], @@ -92,47 +98,61 @@ export const ChatActions: React.FC = () => { if (dataPermissions) { setUserIsmoderator(dataPermissions.user_current[0].isModerator); setMeetingIsBreakout(dataPermissions.meeting[0].isBreakout); + if (!isEmpty(dataPermissions.user_welcomeMsgs[0].welcomeMsg)) { + setShowShowWelcomeMessages(true); + } } }, [dataPermissions]); - const actions = useMemo(()=>{ - const dropdownActions = [ - { - key: uniqueIdsRef.current[0], - enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT, - icon: 'download', - dataTest: 'chatSave', - label: intl.formatMessage(intlMessages.save), - onClick: () => { - getChatMessageHistory(); - downloadOrCopyRef.current = 'download'; - }, - }, - { - key: uniqueIdsRef.current[1], - enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT, - icon: 'copy', - id: 'clipboardButton', - dataTest: 'chatCopy', - label: intl.formatMessage(intlMessages.copy), - onClick: () => { - getChatMessageHistory(); - downloadOrCopyRef.current = 'copy'; - }, - }, - { - key: uniqueIdsRef.current[2], - enable: userIsModerator && !meetingIsBreakout, - icon: 'download', - dataTest: 'chatClear', - label: intl.formatMessage(intlMessages.clear), - onClick: () => clearPublicChatHistory(), - }, - ]; - return dropdownActions.filter((action) => action.enable); - },[userIsModerator, meetingIsBreakout]); - if (errorHistory) return

Error loading chat history: {JSON.stringify(errorHistory)}

; - if (errorPermissions) return

Error loading permissions: {JSON.stringify(errorPermissions)}

; + const actions = useMemo(() => { + const dropdownActions = [ + { + key: uniqueIdsRef.current[0], + enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT, + icon: 'download', + dataTest: 'chatSave', + label: intl.formatMessage(intlMessages.save), + onClick: () => { + getChatMessageHistory(); + downloadOrCopyRef.current = 'download'; + }, + }, + { + key: uniqueIdsRef.current[1], + enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT, + icon: 'copy', + id: 'clipboardButton', + dataTest: 'chatCopy', + label: intl.formatMessage(intlMessages.copy), + onClick: () => { + getChatMessageHistory(); + downloadOrCopyRef.current = 'copy'; + }, + }, + { + key: uniqueIdsRef.current[2], + enable: userIsModerator && !meetingIsBreakout, + icon: 'download', + dataTest: 'chatClear', + label: intl.formatMessage(intlMessages.clear), + onClick: () => clearPublicChatHistory(), + }, + { + key: uniqueIdsRef.current[4], + enable: showShowWelcomeMessages, + icon: 'about', + dataTest: 'restoreWelcomeMessages', + label: intl.formatMessage(intlMessages.showWelcomeMessage), + onClick: () => { + const restoreWelcomeMessagesEvent = new CustomEvent(Events.RESTORE_WELCOME_MESSAGES); + window.dispatchEvent(restoreWelcomeMessagesEvent); + }, + }, + ]; + return dropdownActions.filter((action) => action.enable); + }, [userIsModerator, meetingIsBreakout]); + if (errorHistory) return

Error loading chat history: {JSON.stringify(errorHistory)}

; + if (errorPermissions) return

Error loading permissions: {JSON.stringify(errorPermissions)}

; return ( { } opts={{ id: 'chat-options-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' }, - }} - actions={actions} + keepMounted: true, + transitionDuration: 0, + elevation: 3, + getContentAnchorEl: null, + fullwidth: 'true', + anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, + transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, + }} + actions={actions} /> ); } \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/queries.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/queries.ts index 103e7315e3..9fc0a3cdcd 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/queries.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/queries.ts @@ -10,8 +10,9 @@ export type getChatMessageHistory = { }; export type getPermissions = { - user_current: Array, - meeting: Array<{isBreakout: boolean}> + user_current: Array; + meeting: Array<{ isBreakout: boolean }>; + user_welcomeMsgs: Array<{ welcomeMsg: string; welcomeMsgForModerators: string | null }>; }; export const GET_CHAT_MESSAGE_HISTORY = gql` @@ -37,13 +38,17 @@ query getChatMessageHistory { `; export const GET_PERMISSIONS = gql` -query getPermissions { - user_current { - isModerator + query getPermissions { + user_current { + isModerator + } + meeting { + isBreakout + name + } + user_welcomeMsgs { + welcomeMsg + welcomeMsgForModerators + } } - meeting { - isBreakout - name - } -} `; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/component.tsx index 0639157b08..baf45fa25f 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/component.tsx @@ -1,13 +1,13 @@ import React from 'react'; import Header from '/imports/ui/components/common/control-header/component'; -import { useQuery } from '@apollo/client'; -import { GET_CHAT_DATA, GetChatDataResponse } from './queries'; +import { useMutation, useQuery } from '@apollo/client'; +import { GET_CHAT_DATA, GetChatDataResponse, CLOSE_PRIVATE_CHAT_MUTATION } from './queries'; import { defineMessages, useIntl } from 'react-intl'; import { closePrivateChat } from './services'; import { layoutSelect, layoutDispatch } from '../../../layout/context'; import { useShortcutHelp } from '../../../shortcut-help/useShortcutHelp'; import { Layout } from '../../../layout/layoutTypes'; -import { ACTIONS, PANELS } from '../../../layout/enums'; +import { ACTIONS, PANELS } from '../../../layout/enums'; import { ChatActions } from './chat-actions/component'; interface ChatHeaderProps { @@ -35,59 +35,61 @@ const intlMessages = defineMessages({ }, }); -const ChatHeader: React.FC = ({ chatId, isPublicChat, title}) => { - +const ChatHeader: React.FC = ({ chatId, isPublicChat, title }) => { + const HIDE_CHAT_AK = useShortcutHelp('hideprivatechat'); const CLOSE_CHAT_AK = useShortcutHelp('closeprivatechat'); const layoutContextDispatch = layoutDispatch(); const intl = useIntl(); + const [updateVisible] = useMutation(CLOSE_PRIVATE_CHAT_MUTATION); return (
{ - 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: () => { - closePrivateChat(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? : null} - /> + data-test="chatTitle" + 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: () => { + updateVisible({ variables: { chatId } }); + closePrivateChat(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 ? : null} + /> ); }; @@ -95,7 +97,7 @@ const isChatResponse = (data: unknown): data is GetChatDataResponse => { return (data as GetChatDataResponse).chat !== undefined; }; -const ChatHeaderContainer: React.FC = () => { +const ChatHeaderContainer: React.FC = () => { const intl = useIntl(); const idChatOpen = layoutSelect((i: Layout) => i.idChatOpen); const { diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/queries.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/queries.ts index ae0142073d..6f94928b14 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/queries.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/queries.ts @@ -6,20 +6,28 @@ type chatContent = { participant: { name: string; }; -} +}; export interface GetChatDataResponse { chat: Array; -} +} export const GET_CHAT_DATA = gql` -query GetChatData($chatId: String!) { - chat(where: {chatId: {_eq: $chatId }}) { - chatId - public - participant { - name + query GetChatData($chatId: String!) { + chat(where: { chatId: { _eq: $chatId } }) { + chatId + public + participant { + name + } } } -} -`; \ No newline at end of file +`; + +export const CLOSE_PRIVATE_CHAT_MUTATION = gql` + mutation UpdateChatUser($chatId: String) { + update_chat_user(where: { chatId: { _eq: $chatId } }, _set: { visible: false }) { + affected_rows + } + } +`; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/chat-offline-indicator/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/chat-offline-indicator/component.tsx new file mode 100644 index 0000000000..4c3e48185d --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/chat-offline-indicator/component.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import Styled from './styles'; +import { defineMessages, useIntl } from "react-intl"; + +interface ChatOfflineIndicatorProps { + participantName: string; +} + +const intlMessages = defineMessages({ + partnerDisconnected: { + id: 'app.chat.partnerDisconnected', + description: 'System chat message when the private chat partnet disconnect from the meeting', + }, +}); + +const ChatOfflineIndicator: React.FC = ({ + participantName, +}) => { + const intl = useIntl(); + return ( + + + {intl.formatMessage(intlMessages.partnerDisconnected, { 0: participantName })} + + + ); +}; + +export default ChatOfflineIndicator; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/chat-offline-indicator/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/chat-offline-indicator/styles.ts new file mode 100644 index 0000000000..7824af93a2 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/chat-offline-indicator/styles.ts @@ -0,0 +1,21 @@ +import styled from 'styled-components'; +import { colorText, colorGrayLighter } from '/imports/ui/stylesheets/styled-components/palette'; +export const ChatOfflineIndicator = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + position: relative; + margin-top: 0.2rem; + padding: 0.5rem; + border-radius: 2px; + border-top: 1px solid ${colorGrayLighter}; + & > span { + color: ${colorText}; + font-size: 1rem; + } +`; + +export default { + ChatOfflineIndicator, +}; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/component.tsx index 4b392e6874..e4001f4ff4 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/component.tsx @@ -7,22 +7,25 @@ import ClickOutside from '/imports/ui/components/click-outside/component'; import Styled from './styles'; import { escapeHtml } from '/imports/utils/string-utils'; import { checkText } from 'smile2emoji'; -import TypingIndicatorContainer from '/imports/ui/components/chat/message-form/typing-indicator/container'; import deviceInfo from '/imports/utils/deviceInfo'; import { usePreviousValue } from '/imports/ui/components/utils/hooks'; +import useChat from '/imports/ui/core/hooks/useChat'; import { handleSendMessage, startUserTyping, stopUserTyping, } from './service'; +import { Chat } from '/imports/ui/Types/chat'; +import { Layout } from '../../../layout/layoutTypes'; +import { useMeeting } from '/imports/ui/core/hooks/useMeeting'; +import Events from '/imports/ui/core/events/events'; +import ChatOfflineIndicator from './chat-offline-indicator/component'; interface ChatMessageFormProps { minMessageLength: number, maxMessageLength: number, idChatOpen: string, - chatAreaId: string, chatId: string, - chatTitle: string, connected: boolean, disabled: boolean, locked: boolean, @@ -69,6 +72,18 @@ const messages = defineMessages({ id: 'app.chat.severalPeople', description: 'displayed when 4 or more users are typing', }, + titlePublic: { + id: 'app.chat.titlePublic', + description: 'Public chat title', + }, + titlePrivate: { + id: 'app.chat.titlePrivate', + description: 'Private chat title', + }, + partnerDisconnected: { + id: 'app.chat.partnerDisconnected', + description: 'System chat message when the private chat partnet disconnect from the meeting', + }, }); const CHAT_CONFIG = Meteor.settings.public.chat; @@ -78,7 +93,6 @@ const ENABLE_TYPING_INDICATOR = CHAT_CONFIG.typingIndicator.enabled; const ChatMessageForm: React.FC = ({ handleClickOutside, - chatTitle, title, disabled, idChatOpen, @@ -98,11 +112,9 @@ const ChatMessageForm: React.FC = ({ const textAreaRef = useRef(); const { isMobile } = deviceInfo; const prevChatId = usePreviousValue(chatId); - const messageRef = useRef(); messageRef.current = message; - - const updateUnreadMessages = (chatId, message) => { + const updateUnreadMessages = (chatId: string, message: string) => { const storedData = localStorage.getItem('unsentMessages') || '{}'; const unsentMessages = JSON.parse(storedData); unsentMessages[chatId] = message; @@ -118,14 +130,14 @@ const ChatMessageForm: React.FC = ({ return () => { const unsentMessage = messageRef.current; updateUnreadMessages(chatId, unsentMessage); - } + } }, []); useEffect(() => { const storedData = localStorage.getItem('unsentMessages') || '{}'; const unsentMessages = JSON.parse(storedData); - if(prevChatId) { + if (prevChatId) { updateUnreadMessages(prevChatId, message); } @@ -178,17 +190,19 @@ const ChatMessageForm: React.FC = ({ updateUnreadMessages(chatId, ''); setHasErrors(false); setShowEmojiPicker(false); - if(ENABLE_TYPING_INDICATOR) stopUserTyping(); + if (ENABLE_TYPING_INDICATOR) stopUserTyping(); + const sentMessageEvent = new CustomEvent(Events.SENT_MESSAGE); + window.dispatchEvent(sentMessageEvent); } - const handleEmojiSelect = (emojiObject: { native: string} ) => { + const handleEmojiSelect = (emojiObject: { native: string }) => { const txtArea = textAreaRef?.current?.textarea; const cursor = txtArea.selectionStart; - + setMessage( message.slice(0, cursor) - + emojiObject.native - + message.slice(cursor) + + emojiObject.native + + message.slice(cursor) ); const newCursor = cursor + emojiObject.native.length; @@ -198,7 +212,6 @@ const ChatMessageForm: React.FC = ({ const handleMessageChange = (e: React.FormEvent) => { let newMessage = null; let newError = null; - if (AUTO_CONVERT_EMOJI) { newMessage = checkText(e.target.value); } else { @@ -215,7 +228,7 @@ const ChatMessageForm: React.FC = ({ setMessage(newMessage); setError(newError); - throttledHandleUserTyping(newError); + handleUserTyping(newError) } const handleUserTyping = (error?: boolean) => { @@ -223,9 +236,6 @@ const ChatMessageForm: React.FC = ({ startUserTyping(chatId); } - const throttledHandleUserTyping = throttle(() => handleUserTyping(), - 2000, { trailing: false }); - const handleMessageKeyDown = (e: React.FormEvent) => { // TODO Prevent send message pressing enter on mobile and/or virtual keyboard if (e.keyCode === 13 && !e.shiftKey) { @@ -262,7 +272,7 @@ const ChatMessageForm: React.FC = ({ id="message-input" ref={textAreaRef} placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })} - aria-label={intl.formatMessage(messages.inputLabel, { 0: chatTitle })} + aria-label={intl.formatMessage(messages.inputLabel, { 0: title })} aria-invalid={hasErrors ? 'true' : 'false'} autoCorrect="off" autoComplete="off" @@ -302,7 +312,14 @@ const ChatMessageForm: React.FC = ({ data-test="sendMessageButton" /> - + { + error && ( + + {error} + + ) + } + ); } @@ -317,17 +334,45 @@ const ChatMessageForm: React.FC = ({ }; const ChatMessageFormContainer: React.FC = ({ - chatAreaId, - chatId, - chatTitle, - connected, - disabled, - partnerIsLoggedOut, - title, - locked, + // connected, move to network status }) => { + const intl = useIntl(); const [showEmojiPicker, setShowEmojiPicker] = React.useState(false); - const idChatOpen = layoutSelect((i) => i.idChatOpen); + const idChatOpen: string = layoutSelect((i: Layout) => i.idChatOpen); + const chat = useChat((c: Partial) => { + const participant = c?.participant ? { + participant: { + name: c?.participant?.name, + isModerator: c?.participant?.isModerator, + isOnline: c?.participant?.isOnline, + } + } : {}; + + return { + ...participant, + chatId: c?.chatId, + public: c?.public, + }; + }, idChatOpen) as Partial; + + const title = chat?.participant?.name + ? intl.formatMessage(messages.titlePrivate, { 0: chat?.participant?.name }) + : intl.formatMessage(messages.titlePublic); + + + const meeting = useMeeting((m) => { + return { + lockSettings: { + hasActiveLockSetting: m?.lockSettings?.hasActiveLockSetting, + disablePublicChat: m?.lockSettings?.disablePublicChat, + disablePrivateChat: m?.lockSettings?.disablePrivateChat, + } + }; + }); + + const locked = chat?.public + ? meeting?.lockSettings?.disablePublicChat + : meeting?.lockSettings?.disablePrivateChat; const handleClickOutside = () => { if (showEmojiPicker) { @@ -335,20 +380,23 @@ const ChatMessageFormContainer: React.FC = ({ } }; + if (chat?.participant && !chat.participant.isOnline) { + return ; + } + return ; }; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/service.js b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/service.js deleted file mode 100644 index 20e844bfb3..0000000000 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/service.js +++ /dev/null @@ -1,58 +0,0 @@ -import Auth from '/imports/ui/services/auth'; -import Storage from '/imports/ui/services/storage/session'; -import { makeCall } from '/imports/ui/services/api'; -import { indexOf, without } from '/imports/utils/array-utils'; - -const CHAT_CONFIG = Meteor.settings.public.chat; - -const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; -const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; -const CHAT_EMPHASIZE_TEXT = CHAT_CONFIG.moderatorChatEmphasized; - -const UnsentMessagesCollection = new Mongo.Collection(null); -export const UserSentMessageCollection = new Mongo.Collection(null); - -// session for closed chat list -const CLOSED_CHAT_LIST_KEY = 'closedChatList'; - -const setUserSentMessage = (bool) => { - UserSentMessageCollection.upsert( - { userId: Auth.userID }, - { $set: { sent: bool } }, - ); -}; - -const sendGroupMessage = (message, idChatOpen) => { - const { userID: senderUserId } = Auth; - const chatID = idChatOpen === PUBLIC_CHAT_ID - ? PUBLIC_GROUP_CHAT_ID - : idChatOpen; - - const receiverId = { id: chatID }; - - const payload = { - correlationId: `${senderUserId}-${Date.now()}`, - sender: { - id: senderUserId, - name: '', - role: '', - }, - chatEmphasizedText: CHAT_EMPHASIZE_TEXT, - message, - }; - - const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY); - - // Remove the chat that user send messages from the session. - if (indexOf(currentClosedChats, receiverId.id) > -1) { - Storage.setItem(CLOSED_CHAT_LIST_KEY, without(currentClosedChats, receiverId.id)); - } - - return makeCall('sendGroupChatMsg', chatID, payload); -}; - -export default { - setUserSentMessage, - sendGroupMessage, - UnsentMessagesCollection, -}; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/service.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/service.ts index 749d295d3e..0404e63861 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/service.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/service.ts @@ -9,16 +9,14 @@ const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; const CHAT_EMPHASIZE_TEXT = CHAT_CONFIG.moderatorChatEmphasized; -const START_TYPING_THROTTLE_INTERVAL = 2000; +const START_TYPING_THROTTLE_INTERVAL = 1000; // session for closed chat list const CLOSED_CHAT_LIST_KEY = 'closedChatList'; export const sendGroupMessage = (message: string, idChatOpen: string) => { const { userID: senderUserId } = Auth; - const chatID = idChatOpen === PUBLIC_CHAT_ID - ? PUBLIC_GROUP_CHAT_ID - : idChatOpen; + const chatID = idChatOpen === PUBLIC_CHAT_ID ? PUBLIC_GROUP_CHAT_ID : idChatOpen; const receiverId = { id: chatID }; @@ -43,14 +41,16 @@ export const sendGroupMessage = (message: string, idChatOpen: string) => { return makeCall('sendGroupChatMsg', chatID, payload); }; - export const handleSendMessage = (message: string, idChatOpen: string) => { return sendGroupMessage(message, idChatOpen); }; export const startUserTyping = throttle( - (chatId: string) => makeCall('startUserTyping', chatId), + (chatId: string) => { + makeCall('startUserTyping', chatId); + }, START_TYPING_THROTTLE_INTERVAL, + { leading: true, trailing: false } ); export const stopUserTyping = () => makeCall('stopUserTyping'); diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts index 353007f54c..995e8f64fe 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts @@ -4,6 +4,8 @@ import { colorText, colorGrayLighter, colorPrimary, + colorDanger, + colorGrayDark, } from '/imports/ui/stylesheets/styled-components/palette'; import { smPaddingX, @@ -22,7 +24,6 @@ const Form = styled.form` align-self: flex-end; width: 100%; position: relative; - margin-bottom: calc(-1 * ${smPaddingX}); margin-top: .2rem; `; @@ -120,6 +121,19 @@ const EmojiPickerWrapper = styled.div` } `; + +const Error = styled.div` + color: ${colorDanger}; + font-size: calc(${fontSizeBase} * .75); + color: ${colorGrayDark}; + text-align: left; + padding: ${borderSize} 0; + word-break: break-word; + position: relative; + margin-right: 0.05rem; + margin-left: 0.05rem; +`; + const EmojiPicker = styled(EmojiPickerComponent)``; export default { @@ -131,4 +145,5 @@ export default { EmojiButtonWrapper, EmojiPicker, EmojiPickerWrapper, + Error, }; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx index e238a0fbae..a6f1568603 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx @@ -1,7 +1,7 @@ -import React, { useEffect } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { Meteor } from "meteor/meteor"; -import { useSubscription } from "@apollo/client"; -import { CHAT_SUBSCRIPTION, ChatSubscriptionResponse } from "./queries"; +import { makeVar, useMutation } from "@apollo/client"; +import { LAST_SEEN_MUTATION } from "./queries"; import { ButtonLoadMore, MessageList, @@ -10,6 +10,13 @@ import { import { layoutSelect } from "../../../layout/context"; import ChatListPage from "./page/component"; import { defineMessages, useIntl } from "react-intl"; +import Events from "/imports/ui/core/events/events"; +import useChat from "/imports/ui/core/hooks/useChat"; +import { Chat } from "/imports/ui/Types/chat"; +import { Message } from "/imports/ui/Types/message"; +import { useCurrentUser } from "/imports/ui/core/hooks/useCurrentUser"; +import { User } from "/imports/ui/Types/user"; +import ChatPopupContainer from "../chat-popup/component"; const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id; @@ -27,6 +34,10 @@ const intlMessages = defineMessages({ interface ChatListProps { totalPages: number; chatId: string; + currentUserId: string; + setMessageAsSeenMutation: Function; + totalUnread?: number; + lastSeenAt: number; } const isElement = (el: any): el is HTMLElement => { return el instanceof HTMLElement; @@ -35,18 +46,24 @@ const isElement = (el: any): el is HTMLElement => { const isMap = (map: any): map is Map => { return map instanceof Map; } +let elHeight = 0; -const scrollObserver = new ResizeObserver((entries)=>{ +const scrollObserver = new ResizeObserver((entries) => { for (const entry of entries) { const el = entry.target; - if (isElement(el) && isElement(el.parentElement){ - el.parentElement.scrollTop = el.parentElement.scrollHeight; + if (isElement(el) && isElement(el.parentElement)) { + if (el.offsetHeight > elHeight) { + elHeight = el.offsetHeight; + el.parentElement.scrollTop = el.parentElement.scrollHeight + el.parentElement.clientHeight; + } else { + elHeight = 0; + } } } }); -const setLastSender = (lastSenderPerPage: Map,) =>{ - +const setLastSender = (lastSenderPerPage: Map,) => { + return (page: number, sender: string) => { if (isMap(lastSenderPerPage)) { lastSenderPerPage.set(page, sender); @@ -54,34 +71,121 @@ const setLastSender = (lastSenderPerPage: Map,) =>{ } } -const ChatList: React.FC = ({ totalPages, chatId }) => { +const lastSeenQueue = makeVar<{ [key: string]: Set }>({}); +const setter = makeVar<{ [key: string]: Function }>({}); +const lastSeenAtVar = makeVar<{ [key: string]: number }>({}); +const chatIdVar = makeVar(''); + +const dispatchLastSeen = () => setTimeout(() => { + + const lastSeenQueueValue = lastSeenQueue(); + if (lastSeenQueueValue[chatIdVar()]) { + + const lastTimeQueue = Array.from(lastSeenQueueValue[chatIdVar()]) + const lastSeenTime = Math.max(...lastTimeQueue); + const lastSeenAtVarValue = lastSeenAtVar(); + if (lastSeenTime > (lastSeenAtVarValue[chatIdVar()] ?? 0)) { + lastSeenAtVar({ ...lastSeenAtVar(), [chatIdVar()]: lastSeenTime }); + setter()[chatIdVar()](lastSeenTime); + } + } +}, 500); + +const ChatMessageList: React.FC = ({ + totalPages, + chatId, + setMessageAsSeenMutation, + lastSeenAt, +}) => { const intl = useIntl(); const messageListRef = React.useRef(); const contentRef = React.useRef(); // I used a ref here because I don't want to re-render the component when the last sender changes const lastSenderPerPage = React.useRef>(new Map()); - - const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = React.useState(null); + const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = useState(null); + const [lastMessageCreatedTime, setLastMessageCreatedTime] = useState(0); const [followingTail, setFollowingTail] = React.useState(true); + useEffect(() => { + setter({ + ...setter(), + [chatId]: setLastMessageCreatedTime, + }); + chatIdVar(chatId); + setLastMessageCreatedTime(0); + }, [chatId]); + useEffect(() => { + setMessageAsSeenMutation({ + variables: { + chatId: chatId, + lastSeenAt: lastMessageCreatedTime, + }, + }); + }, [lastMessageCreatedTime]); + + const markMessageAsSeen = useCallback((message: Message) => { + if (message.createdTime > (lastMessageCreatedTime ?? 0)) { + dispatchLastSeen(); + const lastSeenQueueValue = lastSeenQueue(); + if (lastSeenQueueValue[chatId]) { + lastSeenQueueValue[chatId].add(message.createdTime); + lastSeenQueue(lastSeenQueueValue) + } else { + lastSeenQueueValue[chatId] = new Set([message.createdTime]); + lastSeenQueue(lastSeenQueueValue) + } + } + }, [lastMessageCreatedTime, chatId]); + + const setScrollToTailEventHandler = (el: HTMLDivElement) => { + if (Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) === 0) { + if (isElement(contentRef.current)) { + toggleFollowingTail(true) + } + } else { + if (isElement(contentRef.current)) { + toggleFollowingTail(false) + } + } + }; const toggleFollowingTail = (toggle: boolean) => { setFollowingTail(toggle); if (toggle) { if (isElement(contentRef.current)) { scrollObserver.observe(contentRef.current as HTMLDivElement); + setFollowingTail(true); } } else { if (isElement(contentRef.current)) { if (userLoadedBackUntilPage === null) { - setUserLoadedBackUntilPage(Math.max(totalPages-2, 0)); + setUserLoadedBackUntilPage(Math.max(totalPages - 2, 0)); } scrollObserver.unobserve(contentRef.current as HTMLDivElement); + setFollowingTail(false); } } - } + }; useEffect(() => { - if (followingTail){ + const setScrollToTailEventHandler = () => { + if (scrollObserver && contentRef.current) { + scrollObserver.observe(contentRef.current as HTMLDivElement); + if (isElement(messageListRef.current)) { + messageListRef.current.scrollTop = messageListRef.current.scrollHeight + messageListRef.current.clientHeight; + } + setFollowingTail(true); + } + + }; + window.addEventListener(Events.SENT_MESSAGE, setScrollToTailEventHandler); + + return () => { + window.removeEventListener(Events.SENT_MESSAGE, setScrollToTailEventHandler); + } + }, [contentRef.current]); + + useEffect(() => { + if (followingTail) { setUserLoadedBackUntilPage(null); } }, [followingTail]); @@ -95,110 +199,108 @@ const ChatList: React.FC = ({ totalPages, chatId }) => { messageListRef.current.scrollTop = messageListRef.current.scrollHeight; } - return ()=>{ + return () => { toggleFollowingTail(false); } }, [contentRef]); const firstPageToLoad = userLoadedBackUntilPage !== null - ? userLoadedBackUntilPage : Math.max(totalPages-2, 0); - const pagesToLoad = (totalPages-firstPageToLoad) || 1; + ? userLoadedBackUntilPage : Math.max(totalPages - 2, 0); + const pagesToLoad = (totalPages - firstPageToLoad) || 1; return ( { - const el = messageListRef.current as HTMLDivElement; - if (e.deltaY < 0 && el.scrollTop) { - if (isElement(contentRef.current)) { + onWheel={(e) => { + if (e.deltaY < 0) { + if (isElement(contentRef.current) && followingTail) { toggleFollowingTail(false) } } else if (e.deltaY > 0) { - if (Math.abs(el.scrollHeight-el.clientHeight-el.scrollTop) === 0) { - if (isElement(contentRef.current)) { - toggleFollowingTail(true) - } - } + setScrollToTailEventHandler(messageListRef.current as HTMLDivElement); } }} - onMouseUp={(e)=>{ - const el = messageListRef.current as HTMLDivElement; - - if (Math.abs(el.scrollHeight-el.clientHeight-el.scrollTop) === 0) { - if (isElement(contentRef.current)) { - toggleFollowingTail(true) - } - } else { - if (isElement(contentRef.current)) { - toggleFollowingTail(false) - } - } - } + onMouseUp={(e) => { + setScrollToTailEventHandler(messageListRef.current as HTMLDivElement); + }} + onTouchEnd={(e) => { + setScrollToTailEventHandler(messageListRef.current as HTMLDivElement); + }} > { - (userLoadedBackUntilPage) - ? ( - { - if (followingTail){ - toggleFollowingTail(false); - } - setUserLoadedBackUntilPage(userLoadedBackUntilPage-1); - } - } - > - {intl.formatMessage(intlMessages.loadMoreButtonLabel)} - - ): null + (userLoadedBackUntilPage) + ? ( + { + if (followingTail) { + toggleFollowingTail(false); + } + setUserLoadedBackUntilPage(userLoadedBackUntilPage - 1); + } + } + > + {intl.formatMessage(intlMessages.loadMoreButtonLabel)} + + ) : null }
+ { - Array.from({length: pagesToLoad }, (v, k) => k+(firstPageToLoad)).map((page) => { - console.log('page', page); - + Array.from({ length: pagesToLoad }, (v, k) => k + (firstPageToLoad)).map((page) => { return ( ) }) }
-
+ ); } -const ChatListContainer: React.FC = ({}) => { +const ChatMessageListContainer: React.FC = ({ }) => { const idChatOpen = layoutSelect((i) => i.idChatOpen); const isPublicChat = idChatOpen === PUBLIC_CHAT_KEY; const chatId = !isPublicChat ? idChatOpen : PUBLIC_GROUP_CHAT_KEY; - const { - data: chatData, - loading: chatLoading, - error: chatError, - } = useSubscription(CHAT_SUBSCRIPTION); - // We verify if the chat is loading to avoid fetching uneccessary messages - // and we use MessageListWrapper to fill the space in interface while loading. - if (chatLoading) return ; - if (chatError) return

chatError: {chatError}

; - const currentChat = chatData?.chat?.find((chat) => chat?.chatId === chatId); + const currentChat = useChat((chat) => { + return { + chatId: chat.chatId, + totalMessages: chat.totalMessages, + totalUnread: chat.totalUnread, + lastSeenAt: chat.lastSeenAt, + } + }, chatId) as Partial; + const currentUser = useCurrentUser((user: Partial) => { + return { + userId: user.userId, + } + }); + const [setMessageAsSeenMutation] = useMutation(LAST_SEEN_MUTATION); + const totalMessages = currentChat?.totalMessages || 0; const totalPages = Math.ceil(totalMessages / PAGE_SIZE); return ( - ); } -export default ChatListContainer; \ No newline at end of file +export default ChatMessageListContainer; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/componet.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/componet.tsx index 1bb0611d45..32d95b5905 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/componet.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/componet.tsx @@ -1,80 +1,166 @@ -import React, { Ref, useEffect } from "react"; +import React, { Ref, useCallback, useEffect, useMemo, useRef } from "react"; import { Message } from '/imports/ui/Types/message'; -import { FormattedTime, defineMessages, useIntl } from 'react-intl'; import { - ChatAvatar, - ChatTime, - ChatUserContent, - ChatUserName, ChatWrapper, - ChatUserOffline, - ChatMessage, ChatContent, } from "./styles"; +import ChatMessageHeader from "./message-header/component"; +import ChatMessageTextContent from "./message-content/text-content/component"; +import ChatPollContent from "./message-content/poll-content/component"; +import ChatMessagePresentationContent from "./message-content/presentation-content/component"; +import { defineMessages, useIntl } from "react-intl"; interface ChatMessageProps { message: Message; previousMessage?: Message; lastSenderPreviousPage?: string | null; + scrollRef: React.RefObject; + markMessageAsSeen: Function; +} + +const enum MessageType { + TEXT = 'default', + POLL = 'poll', + PRESENTATION = 'presentation', + CHAT_CLEAR = 'publicChatHistoryCleared' } const intlMessages = defineMessages({ - offline: { - id: 'app.chat.offline', - description: 'Offline', + pollResult: { + id: 'app.chat.pollResult', + description: 'used in place of user name who published poll to chat', + }, + presentationLabel: { + id: 'app.presentationUploder.title', + description: 'presentation area element label', + }, + systemLabel: { + id: 'app.toast.chat.system', + description: 'presentation area element label', + }, + chatClear: { + id: 'app.chat.clearPublicChatMessage', + description: 'message of when clear the public chat', }, }); -const ChatMesssage: React.FC = ({ message, previousMessage, lastSenderPreviousPage}) => { +function isInViewport(el: HTMLDivElement) { + const rect = el.getBoundingClientRect(); + + return ( + rect.top <= (window.innerHeight || document.documentElement.clientHeight) && + rect.bottom >= 0 + ); +} + +const ChatMesssage: React.FC = ({ + message, + previousMessage, + lastSenderPreviousPage, + scrollRef, + markMessageAsSeen, +}) => { const intl = useIntl(); + const messageRef = useRef(null); + const markMessageAsSeenOnScrollEnd = useCallback((message, messageRef) => { + if (messageRef.current && isInViewport(messageRef.current)) { + markMessageAsSeen(message); + } + }, []); + + useEffect(() => { + // I use a function here to remove the event listener using the same reference + const callbackFunction = () => { + markMessageAsSeenOnScrollEnd(message, messageRef); + } + if (message && scrollRef.current && messageRef.current) { + if (isInViewport(messageRef.current)) { + markMessageAsSeen(message); + } else { + scrollRef.current.addEventListener('scrollend', callbackFunction); + } + } + return () => { + scrollRef?.current + ?.removeEventListener('scrollend', callbackFunction); + } + }, [message, messageRef]); + if (!message) return null; + const sameSender = (previousMessage?.user?.userId || lastSenderPreviousPage) === message?.user?.userId; const dateTime = new Date(message?.createdTime); + const messageContent: { + name: string, + color: string, + isModerator: boolean, + component: React.ReactElement, + } = useMemo(() => { + switch (message.messageType) { + case MessageType.POLL: + return { + name: intl.formatMessage(intlMessages.pollResult), + color: '#3B48A9', + isModerator: true, + component: ( + + ), + }; + case MessageType.PRESENTATION: + return { + name: intl.formatMessage(intlMessages.presentationLabel), + color: '#0F70D7', + isModerator: true, + component: ( + + ), + }; + case MessageType.CHAT_CLEAR: + return { + name: intl.formatMessage(intlMessages.systemLabel), + color: '#0F70D7', + isModerator: true, + component: ( + + ), + }; + case MessageType.TEXT: + default: + return { + name: message.user?.name, + color: message.user?.color, + isModerator: message.user?.isModerator, + component: ( + + ), + } + } + }, []); return ( - { - sameSender ? null : ( - - {message.user?.name.toLowerCase().slice(0, 2) || " "} - - ) - } + - {sameSender ? null : - ( - - - {message.user?.name} - - { - message.user?.isOnline ? null : ( - - {`(${intl.formatMessage(intlMessages.offline)})`} - - ) - } - - - - - - ) - } - - {message.message} - + { + messageContent.component + } ); diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/poll-content/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/poll-content/component.tsx new file mode 100644 index 0000000000..6b297671ef --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/poll-content/component.tsx @@ -0,0 +1,77 @@ +import React from "react"; +import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"; +import Styled from './styles'; + +interface ChatPollContentProps { + metadata: string; +} + +interface Metadata { + id: string; + question: string; + numRespondents: number; + numResponders: number; + questionText: string; + questionType: string; + answers: Array; +} + +interface Answers { + key: string; + numVotes: number; + id: number; +} + + +function assertAsMetadata(metadata: unknown): asserts metadata is Metadata { + if (typeof metadata !== 'object' || metadata === null) { + throw new Error('metadata is not an object'); + } + if (typeof (metadata as Metadata).id !== 'string') { + throw new Error('metadata.id is not a string'); + } + if (typeof (metadata as Metadata).numRespondents !== 'number') { + throw new Error('metadata.numRespondents is not a number'); + } + if (typeof (metadata as Metadata).numResponders !== 'number') { + throw new Error('metadata.numResponders is not a number'); + } + if (typeof (metadata as Metadata).questionText !== 'string') { + throw new Error('metadata.questionText is not a string'); + } + if (typeof (metadata as Metadata).questionType !== 'string') { + throw new Error('metadata.questionType is not a string'); + } + if (!Array.isArray((metadata as Metadata).answers)) { + throw new Error('metadata.answers is not an array'); + } + if ((metadata as Metadata).answers.length === 0) { + throw new Error('metadata.answers is empty'); + } +} + +const ChatPollContent: React.FC = ({ + metadata: string, +}) => { + const pollData = JSON.parse(string) as unknown; + assertAsMetadata(pollData); + return ( +
+ + {pollData.questionText} + + + + + + + + +
+ ); +}; + +export default ChatPollContent; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/poll-content/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/poll-content/styles.ts new file mode 100644 index 0000000000..9d7d1b015b --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/poll-content/styles.ts @@ -0,0 +1,16 @@ +import styled from 'styled-components'; +import { colorText } from '/imports/ui/stylesheets/styled-components/palette'; + +export const pollText = styled.div` + margin-top: 0.5rem; + margin-bottom: 0.5rem; + font-size: 1.25rem; + font-weight: 500; + margin-left: 2.75rem; + color: ${colorText}; + word-break: break-word; +`; + +export default { + pollText, +}; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/presentation-content/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/presentation-content/component.tsx new file mode 100644 index 0000000000..5e839177e4 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/presentation-content/component.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import Styled from './styles'; +import { defineMessages, useIntl } from "react-intl"; +interface ChatMessagePresentationContentProps { + metadata: string; +} +interface Metadata { + fileURI: string; + filename: string; +} + +function assertAsMetadata(metadata: unknown): asserts metadata is Metadata { + if (typeof metadata !== 'object' || metadata === null) { + throw new Error('metadata is not an object'); + } + if (typeof (metadata as Metadata).fileURI !== 'string') { + throw new Error('metadata.fileURI is not a string'); + } + if (typeof (metadata as Metadata).filename !== 'string') { + throw new Error('metadata.fileName is not a string'); + } +} + +const intlMessages = defineMessages({ + download: { + id: 'app.presentation.downloadLabel', + description: 'used as label for presentation download link', + }, + notAccessibleWarning: { + id: 'app.presentationUploader.export.notAccessibleWarning', + description: 'used for indicating that a link may be not accessible', + }, +}); + + +const ChatMessagePresentationContent: React.FC = ({ + metadata: string, +}) => { + const intl = useIntl(); + const presentationData = JSON.parse(string) as unknown; + assertAsMetadata(presentationData); + return ( + + {presentationData.filename} + + {intl.formatMessage(intlMessages.download)} + + + ); +} + +export default ChatMessagePresentationContent; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/presentation-content/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/presentation-content/styles.ts new file mode 100644 index 0000000000..de1770c69b --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/presentation-content/styles.ts @@ -0,0 +1,19 @@ +import styled from 'styled-components'; +import { colorText, colorPrimary } from '/imports/ui/stylesheets/styled-components/palette'; + +export const ChatDowloadContainer = styled.div` + display: flex; + flex-flow: column; + color: ${colorText}; + word-break: break-word; + margin-left: 2.75rem; +`; + +export const ChatLink = styled.a` + color: ${colorPrimary}; +`; + +export default { + ChatDowloadContainer, + ChatLink, +}; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/component.tsx new file mode 100644 index 0000000000..234e956f54 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/component.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import Styled from './styles'; +interface ChatMessageTextContentProps { + text: string; + emphasizedMessage: boolean; +} + +const ChatMessageTextContent: React.FC = ({ + text, + emphasizedMessage, +}) => { + return ( + + {text} + + ); +}; + +export default ChatMessageTextContent; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/styles.ts new file mode 100644 index 0000000000..5ac698cade --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/styles.ts @@ -0,0 +1,20 @@ +import styled from 'styled-components'; +import { colorText } from '/imports/ui/stylesheets/styled-components/palette'; + +export const ChatMessage = styled.div` + flex: 1; + display: flex; + flex-flow: row; + color: ${colorText}; + word-break: break-word; + margin-left: 2.75rem; + ${({ emphasizedMessage }) => + emphasizedMessage && + ` + font-weight: bold; + `} +`; + +export default { + ChatMessage, +}; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-header/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-header/component.tsx new file mode 100644 index 0000000000..c7e4cbeddb --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-header/component.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import Styled from './styles'; +import { useIntl, defineMessages, FormattedTime } from "react-intl"; + + +const intlMessages = defineMessages({ + offline: { + id: 'app.chat.offline', + description: 'Offline', + }, +}); + + +interface ChatMessageHeaderProps { + name: string; + avatar: string; + color: string; + isModerator: boolean; + isOnline: boolean; + dateTime: Date; + sameSender: boolean; +} + +const ChatMessageHeader: React.FC = ({ + sameSender, + name, + color, + isModerator, + avatar, + isOnline, + dateTime, +}) => { + const intl = useIntl(); + if (sameSender) return null; + + return ( + + + {name.toLowerCase().slice(0, 2) || " "} + + + + {name} + + { + isOnline ? null : ( + + {`(${intl.formatMessage(intlMessages.offline)})`} + + ) + } + + + + + + ) +}; + +export default ChatMessageHeader; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-header/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-header/styles.ts new file mode 100644 index 0000000000..04173a80d7 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-header/styles.ts @@ -0,0 +1,190 @@ +import styled, { css } from 'styled-components'; + +import { + borderSize, + userIndicatorsOffset, +} from '/imports/ui/stylesheets/styled-components/general'; +import { + colorWhite, + userListBg, + colorSuccess, + colorHeading, + palettePlaceholderText, + colorGrayLight, + colorText, +} from '/imports/ui/stylesheets/styled-components/palette'; +import { + lineHeightComputed, + fontSizeBase, + btnFontWeight, +} from '/imports/ui/stylesheets/styled-components/typography'; + +export const HeaderContent = styled.div` + display: flex; + flex-flow: row; + width: 100%; +`; + +export const ChatUserName = styled.div` + display: flex; + min-width: 0; + font-weight: 600; + position: relative; + + min-width: 0; + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + ${({ isOnline }) => + isOnline && + ` + color: ${colorHeading}; + `} + + ${({ isOnline }) => + !isOnline && + ` + text-transform: capitalize; + font-style: italic; + + & > span { + text-align: right; + padding: 0 .1rem 0 0; + + [dir="rtl"] & { + text-align: left; + padding: 0 0 0 .1rem; + } + } + `} +`; + +export const ChatUserOffline = styled.span` + color: ${colorGrayLight}; + font-weight: 100; + text-transform: lowercase; + font-style: italic; + font-size: 90%; + line-height: 1; + user-select: none; + margin: 0 0 0 calc(${lineHeightComputed} / 2); +`; + +export const ChatTime = styled.time` + flex-shrink: 0; + flex-grow: 0; + flex-basis: 3.5rem; + color: ${palettePlaceholderText}; + text-transform: uppercase; + font-size: 75%; + margin: 0 0 0 calc(${lineHeightComputed} / 2); + [dir='rtl'] & { + margin: 0 calc(${lineHeightComputed} / 2) 0 0; + } + + & > span { + vertical-align: sub; + } +`; + +export const ChatAvatar = styled.div` + flex: 0 0 2.25rem; + margin: 0px calc(0.5rem) 0px 0px; + box-flex: 0; + position: relative; + height: 2.25rem; + width: 2.25rem; + border-radius: 50%; + text-align: center; + font-size: .85rem; + border: 2px solid transparent; + user-select: none; + ${({ color }) => css` + background-color: ${color}; + `} + } + + &:after, + &:before { + content: ""; + position: absolute; + width: 0; + height: 0; + padding-top: .5rem; + padding-right: 0; + padding-left: 0; + padding-bottom: 0; + color: inherit; + top: auto; + left: auto; + bottom: ${userIndicatorsOffset}; + right: ${userIndicatorsOffset}; + border: 1.5px solid ${userListBg}; + border-radius: 50%; + background-color: ${colorSuccess}; + color: ${colorWhite}; + opacity: 0; + font-family: 'bbb-icons'; + font-size: .65rem; + line-height: 0; + text-align: center; + vertical-align: middle; + letter-spacing: -.65rem; + z-index: 1; + + [dir="rtl"] & { + left: ${userIndicatorsOffset}; + right: auto; + padding-right: .65rem; + padding-left: 0; + } + } + + ${({ moderator }) => + moderator && + ` + border-radius: 5px; + `} + + // ================ image ================ + ${({ avatar, emoji }) => + avatar?.length !== 0 && + !emoji && + css` + background-image: url(${avatar}); + background-repeat: no-repeat; + background-size: contain; + `} + // ================ image ================ + + // ================ content ================ + color: ${colorWhite}; + font-size: 110%; + text-transform: capitalize; + display: flex; + justify-content: center; + align-items:center; + // ================ content ================ + + & .react-loading-skeleton { + height: 2.25rem; + width: 2.25rem; + } +`; + +export const ChatHeaderText = styled.div` + display: flex; + align-items: baseline; + width: 100%; +`; + +export default { + HeaderContent, + ChatAvatar, + ChatTime, + ChatUserOffline, + ChatUserName, + ChatHeaderText, +}; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts index 4445dd87b6..5bc47db2c6 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts @@ -11,7 +11,7 @@ import { colorHeading, palettePlaceholderText, colorGrayLight, - colorText + colorText, } from '/imports/ui/stylesheets/styled-components/palette'; import { lineHeightComputed, @@ -20,20 +20,23 @@ import { } from '/imports/ui/stylesheets/styled-components/typography'; export const ChatWrapper = styled.div` - pointer-events: auto; - [dir="rtl"] & { + [dir='rtl'] & { direction: rtl; } display: flex; - flex-flow: row; + flex-flow: column; position: relative; - ${({ sameSender }) => sameSender && ` + ${({ sameSender }) => + sameSender && + ` flex: 1; margin: ${borderSize} 0 0 ${borderSize}; margin-top: calc(${lineHeightComputed} / 3); `} - ${({ sameSender }) => !sameSender && ` + ${({ sameSender }) => + !sameSender && + ` padding-top:${lineHeightComputed}; `} [dir="rtl"] & { @@ -42,174 +45,8 @@ export const ChatWrapper = styled.div` font-size: ${fontSizeBase}; `; - - -export const ChatUserContent = styled.div` - display: flex; - flex-flow: row; - width: 100%; -`; - -export const ChatUserName = styled.div` - display: flex; - min-width: 0; - font-weight: 600; - position: relative; - - min-width: 0; - display: inline-block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - ${({ isOnline }) => isOnline && ` - color: ${colorHeading}; - `} - - ${({ isOnline }) => !isOnline && ` - text-transform: capitalize; - font-style: italic; - - & > span { - text-align: right; - padding: 0 .1rem 0 0; - - [dir="rtl"] & { - text-align: left; - padding: 0 0 0 .1rem; - } - } - `} -`; - -export const ChatUserOffline = styled.span` - color: ${colorGrayLight}; - font-weight: 100; - text-transform: lowercase; - font-style: italic; - font-size: 90%; - line-height: 1; - align-self: center; - user-select: none; - margin: 0 0 0 calc(${lineHeightComputed} / 2); -`; - -export const ChatTime = styled.time` - flex-shrink: 0; - flex-grow: 0; - flex-basis: 3.5rem; - color: ${palettePlaceholderText}; - text-transform: uppercase; - font-size: 75%; - margin: 0 0 0 calc(${lineHeightComputed} / 2); - align-self: center; - [dir="rtl"] & { - margin: 0 calc(${lineHeightComputed} / 2) 0 0; - } - - & > span { - vertical-align: sub; - } -`; - export const ChatContent = styled.div` display: flex; flex-flow: column; width: 100%; -` - -export const ChatMessage = styled.div` - flex: 1; - display: flex; - flex-flow: row; - color: ${colorText}; - word-break: break-word; - ${({ sameSender }) => sameSender && ` - margin-left: calc(2.75rem - 2px); - `} - ${({ emphasizedMessage }) => emphasizedMessage && ` - font-weight: bold; - `} `; - - -export const ChatAvatar = styled.div` - flex: 0 0 2.25rem; - margin: 0px calc(0.5rem) 0px 0px; - box-flex: 0; - position: relative; - height: 2.25rem; - width: 2.25rem; - border-radius: 50%; - text-align: center; - font-size: .85rem; - border: 2px solid transparent; - user-select: none; - ${ - ({ color }) => css` - background-color: ${color}; - `} - } - - &:after, - &:before { - content: ""; - position: absolute; - width: 0; - height: 0; - padding-top: .5rem; - padding-right: 0; - padding-left: 0; - padding-bottom: 0; - color: inherit; - top: auto; - left: auto; - bottom: ${userIndicatorsOffset}; - right: ${userIndicatorsOffset}; - border: 1.5px solid ${userListBg}; - border-radius: 50%; - background-color: ${colorSuccess}; - color: ${colorWhite}; - opacity: 0; - font-family: 'bbb-icons'; - font-size: .65rem; - line-height: 0; - text-align: center; - vertical-align: middle; - letter-spacing: -.65rem; - z-index: 1; - - [dir="rtl"] & { - left: ${userIndicatorsOffset}; - right: auto; - padding-right: .65rem; - padding-left: 0; - } - } - - ${({ moderator }) => moderator && ` - border-radius: 5px; - `} - - // ================ image ================ - ${({ avatar, emoji }) => avatar?.length !== 0 && !emoji && css` - background-image: url(${avatar}); - background-repeat: no-repeat; - background-size: contain; - `} - // ================ image ================ - - // ================ content ================ - color: ${colorWhite}; - font-size: 110%; - text-transform: capitalize; - display: flex; - justify-content: center; - align-items:center; - // ================ content ================ - - & .react-loading-skeleton { - height: 2.25rem; - width: 2.25rem; - } -`; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/component.tsx index 1a24d683cf..fb4e72b8b3 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/component.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import { useSubscription } from "@apollo/client"; import { Meteor } from 'meteor/meteor'; import { @@ -19,12 +19,16 @@ interface ChatListPageContainerProps { setLastSender: Function; lastSenderPreviousPage: string | undefined; chatId: string; + markMessageAsSeen: Function; + scrollRef: React.RefObject; } interface ChatListPageProps { messages: Array; lastSenderPreviousPage: string | undefined; page: number; + markMessageAsSeen: Function; + scrollRef: React.RefObject; } const verifyIfIsPublicChat = (message: unknown): message is ChatMessagePublicSubscriptionResponse => { @@ -35,20 +39,27 @@ const verifyIfIsPrivateChat = (message: unknown): message is ChatMessagePrivateS return (message as ChatMessagePrivateSubscriptionResponse).chat_message_private !== undefined; } +const ChatListPage: React.FC = ({ + messages, + lastSenderPreviousPage, + page, + markMessageAsSeen, + scrollRef, -const ChatListPage: React.FC = ({ messages, lastSenderPreviousPage, page }) => { - +}) => { return ( -
+
{ messages.map((message, index, Array) => { - const previousMessage = Array[index-1]; + const previousMessage = Array[index - 1]; return ( ) }) @@ -63,14 +74,16 @@ const ChatListPageContainer: React.FC = ({ setLastSender, lastSenderPreviousPage, chatId, + markMessageAsSeen, + scrollRef, }) => { const isPublicChat = chatId === PUBLIC_GROUP_CHAT_KEY; const chatQuery = isPublicChat ? CHAT_MESSAGE_PUBLIC_SUBSCRIPTION : CHAT_MESSAGE_PRIVATE_SUBSCRIPTION; - const defaultVariables = { offset: (page)*pageSize, limit: pageSize }; + const defaultVariables = { offset: (page) * pageSize, limit: pageSize }; const variables = isPublicChat ? defaultVariables : { ...defaultVariables, requestedChatId: chatId }; - const { + const { data: chatMessageData, loading: chatMessageLoading, error: chatMessageError, @@ -89,7 +102,8 @@ const ChatListPageContainer: React.FC = ({ } if (messages.length > 0) { - setLastSender(page, messages[messages.length-1].user?.userId); + setLastSender(page, messages[messages.length - 1].user?.userId); + } return ( @@ -97,6 +111,8 @@ const ChatListPageContainer: React.FC = ({ messages={messages} lastSenderPreviousPage={lastSenderPreviousPage} page={page} + markMessageAsSeen={markMessageAsSeen} + scrollRef={scrollRef} /> ); } diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/queries.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/queries.ts index dd626c7bb8..20cd20d7c8 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/queries.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/queries.ts @@ -10,40 +10,51 @@ export interface ChatMessagePrivateSubscriptionResponse { chat_message_private: Array; } -export const CHAT_MESSAGE_PUBLIC_SUBSCRIPTION = gql`subscription chatMessages($limit: Int!, $offset: Int!) { - chat_message_public(limit: $limit, offset: $offset, order_by: {createdTime: asc}) { - user { - name - userId - avatar - isOnline - isModerator - color +export const CHAT_MESSAGE_PUBLIC_SUBSCRIPTION = gql` + subscription chatMessages($limit: Int!, $offset: Int!) { + chat_message_public(limit: $limit, offset: $offset, order_by: { createdTime: asc }) { + user { + name + userId + avatar + isOnline + isModerator + color + } + messageType + chatId + message + messageId + createdTime + createdTimeAsDate + messageMetadata } - message - messageId - createdTime - createdTimeAsDate } -} `; -export const CHAT_MESSAGE_PRIVATE_SUBSCRIPTION = gql`subscription chatMessages($limit: Int!, $offset: Int!, $requestedChatId: String!) { - chat_message_private(limit: $limit, offset: $offset, where: {chatId: {_eq: $requestedChatId }} , order_by: {createdTime: asc}) { - user { - name - userId - avatar - isOnline - isModerator - color +export const CHAT_MESSAGE_PRIVATE_SUBSCRIPTION = gql` + subscription chatMessages($limit: Int!, $offset: Int!, $requestedChatId: String!) { + chat_message_private( + limit: $limit + offset: $offset + where: { chatId: { _eq: $requestedChatId } } + order_by: { createdTime: asc } + ) { + user { + name + userId + avatar + isOnline + isModerator + color + } + chatId + message + messageType + messageId + createdTime + createdTimeAsDate + messageMetadata } - message - messageId - createdTime - createdTimeAsDate } -} `; - - diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/queries.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/queries.ts index 1bb1a9cbec..523b256595 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/queries.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/queries.ts @@ -1,23 +1,12 @@ import { gql } from '@apollo/client'; -import { Chat } from '/imports/ui/Types/chat'; -export interface ChatSubscriptionResponse { - chat: Array; -} - -export const CHAT_SUBSCRIPTION = gql` -subscription { - chat { - chatId - participant { - name - role - color - loggedOut - } - totalMessages - totalUnread - public - } -} -` +export const LAST_SEEN_MUTATION = gql` + mutation UpdateChatUser($chatId: String, $lastSeenAt: bigint) { + update_chat_user( + where: { chatId: { _eq: $chatId }, lastSeenAt: { _lt: $lastSeenAt } } + _set: { lastSeenAt: $lastSeenAt } + ) { + affected_rows + } + } +`; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/styles.ts index 51956c2ffc..e787aa901d 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/styles.ts @@ -9,9 +9,9 @@ import { ScrollboxVertical } from '/imports/ui/stylesheets/styled-components/scr import { colorGrayDark } from '/imports/ui/stylesheets/styled-components/palette'; export const MessageListWrapper = styled.div` + height: 100%; display: flex; flex-flow: column; - flex-grow: 1; flex-shrink: 1; position: relative; overflow-x: hidden; @@ -22,7 +22,7 @@ export const MessageListWrapper = styled.div` margin-right: calc(-1 * ${mdPaddingY}); padding-bottom: ${mdPaddingX}; z-index: 2; - [dir="rtl"] & { + [dir='rtl'] & { padding-right: ${mdPaddingX}; margin-right: calc(-1 * ${mdPaddingX}); padding-left: ${mdPaddingY}; @@ -33,30 +33,28 @@ export const MessageListWrapper = styled.div` export const MessageList = styled(ScrollboxVertical)` flex-flow: column; flex-shrink: 1; - margin: 0 auto 0 0; right: 0 ${mdPaddingX} 0 0; padding-top: 0; - width: 100%; outline-style: none; overflow-x: hidden; - [dir="rtl"] & { + [dir='rtl'] & { margin: 0 0 0 auto; padding: 0 0 0 ${mdPaddingX}; } - display:block; + display: block; `; export const ButtonLoadMore = styled.button` width: 100%; min-height: 1.5rem; - margin-bottom: .75rem; + margin-bottom: 0.75rem; background-color: transparent; border-radius: ${borderRadius}; - border: 1px ridge ${colorGrayDark} + border: 1px ridge ${colorGrayDark}; `; export default { MessageListWrapper, MessageList, -} \ No newline at end of file +}; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-popup/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-popup/component.tsx index b658dbadc3..c278d6ccfb 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-popup/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-popup/component.tsx @@ -1,56 +1,106 @@ import React, { useEffect } from 'react'; -import { PopupContainer } from './styles'; +import { PopupContainer, PopupContents } from './styles'; import { GET_WELCOME_MESSAGE, WelcomeMsgsResponse } from './queries'; import { useQuery } from '@apollo/client'; import PopupContent from './popup-content/component'; +import Events from '/imports/ui/core/events/events'; +import { layoutSelect } from '../../../layout/context'; +import { Layout } from '../../../layout/layoutTypes'; interface ChatPopupProps { welcomeMessage?: string | null; welcomeMsgForModerators?: string | null; } +const WELCOME_MSG_KEY = 'welcomeMsg'; +const WELCOME_MSG_FOR_MODERATORS_KEY = 'welcomeMsgForModerators'; +const CHAT_CONFIG = Meteor.settings.public.chat; +const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id; +const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id; + + +const setWelcomeMsgsOnSession = (key: string, value: boolean) => { + sessionStorage.setItem(key, String(value)); +}; + +const isBoolean = (v: any): boolean => { + if (v === 'true') { + return true; + } else if (v === 'false') { + return false; + } + // if v is not difined it shouldn't be considered on comparation, so it returns true + return true; +} + const ChatPopup: React.FC = ({ welcomeMessage, welcomeMsgForModerators, }) => { - const [showWelcomeMessage, setShowWelcomeMessage] = React.useState(false); - const [showWelcomeMessageForModerators, setShowWelcomeMessageForModerators] = React.useState(false); + + const [showWelcomeMessage, setShowWelcomeMessage] = React.useState( + welcomeMessage && isBoolean(sessionStorage.getItem(WELCOME_MSG_KEY)) + ); + const [showWelcomeMessageForModerators, setShowWelcomeMessageForModerators] = React.useState( + welcomeMsgForModerators && isBoolean(sessionStorage.getItem(WELCOME_MSG_FOR_MODERATORS_KEY)) + ); useEffect(() => { - if (welcomeMessage) { - setShowWelcomeMessage(true); - } - if (welcomeMsgForModerators) { - setShowWelcomeMessageForModerators(true); + const eventCallback = () => { + if (welcomeMessage) { + setShowWelcomeMessage(true); + setWelcomeMsgsOnSession(WELCOME_MSG_KEY, true); + } + if (welcomeMsgForModerators) { + setShowWelcomeMessageForModerators(true); + setWelcomeMsgsOnSession(WELCOME_MSG_FOR_MODERATORS_KEY, true); + } + }; + window.addEventListener(Events.RESTORE_WELCOME_MESSAGES, eventCallback); + + return () => { + removeEventListener(Events.RESTORE_WELCOME_MESSAGES, eventCallback); } }, []); if (!showWelcomeMessage && !showWelcomeMessageForModerators) return null; return ( - {showWelcomeMessage && welcomeMessage && ( - setShowWelcomeMessage(false)} - /> - )} - {showWelcomeMessageForModerators && welcomeMsgForModerators && ( - setShowWelcomeMessageForModerators(false)} + + {showWelcomeMessage && welcomeMessage && ( + { + setShowWelcomeMessage(false); + setWelcomeMsgsOnSession(WELCOME_MSG_KEY, false); + }} /> - )} + )} + {showWelcomeMessageForModerators && welcomeMsgForModerators && ( + { + setShowWelcomeMessageForModerators(false); + setWelcomeMsgsOnSession(WELCOME_MSG_FOR_MODERATORS_KEY, false); + }} + /> + )} + + ); }; const ChatPopupContainer: React.FC = () => { const { - data: welcomeData, - loading: welcomeLoading, - error: welcomeError, - } = useQuery(GET_WELCOME_MESSAGE); - if(welcomeLoading) return null; - if(welcomeError) return
{JSON.stringify(welcomeError)}
; - if(!welcomeData) return null; + data: welcomeData, + loading: welcomeLoading, + error: welcomeError, + } = useQuery(GET_WELCOME_MESSAGE); + const idChatOpen = layoutSelect((i: Layout) => i.idChatOpen); + if (idChatOpen !== PUBLIC_GROUP_CHAT_KEY) return null; + + if (welcomeLoading) return null; + if (welcomeError) return
{JSON.stringify(welcomeError)}
; + if (!welcomeData) return null; return ( , indicatorEnabled: boolean, - error: string, -} - -interface TypingIndicatorContainerProps { - userId: string, - isTypingTo: string, - isPrivate: boolean, - error: string, } const messages = defineMessages({ @@ -29,13 +32,11 @@ const messages = defineMessages({ }, }); -const CHAT_CONFIG = Meteor.settings.public.chat; -const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled; + const TypingIndicator: React.FC = ({ typingUsers, - indicatorEnabled, - error, + indicatorEnabled. }) => { const intl = useIntl(); @@ -98,37 +99,62 @@ const TypingIndicator: React.FC = ({ return ( - {error || element} + {element} ); }; -const TypingIndicatorContainer: React.FC = ({ userId, isTypingTo, isPrivate, error }) => { - const { - data: typingUsersData, - } = useSubscription(isPrivate ? IS_TYPING_PRIVATE_SUBSCRIPTION : IS_TYPING_PUBLIC_SUBSCRIPTION, { - variables: { - chatId: isTypingTo, +const TypingIndicatorContainer: React.FC = () => { + + + const idChatOpen: string = layoutSelect((i: Layout) => i.idChatOpen); + const currentUser = useCurrentUser((user: Partial) => { + return { + userId: user.userId, } }); + DEBUG_CONSOLE && console.log('TypingIndicatorContainer:currentUser', currentUser); + const chat = useChat((c: Partial) => { + const participant = c?.participant ? { + participant: { + name: c?.participant?.name, + isModerator: c?.participant?.isModerator, + isOnline: c?.participant?.isOnline, + } + } : {}; + return { + ...participant, + chatId: c?.chatId, + public: c?.public, + }; + }, idChatOpen) as Partial; + DEBUG_CONSOLE && console.log('TypingIndicatorContainer:chat', chat); + const typingQuery = idChatOpen === PUBLIC_GROUP_CHAT_KEY ? IS_TYPING_PUBLIC_SUBSCRIPTION : IS_TYPING_PRIVATE_SUBSCRIPTION; + const { + data: typingUsersData, + error: typingUsersError, + } = useSubscription(typingQuery, { + variables: { + chatId: idChatOpen, + } + }); + DEBUG_CONSOLE && console.log('TypingIndicatorContainer:typingUsersData', typingUsersData); + + if (typingUsersError) return
Error: {JSON.stringify(typingUsersError)}
const publicTypingUsers = typingUsersData?.user_typing_public || []; const privateTypingUsers = typingUsersData?.user_typing_private || []; const typingUsers = privateTypingUsers.concat(publicTypingUsers); const typingUsersArray = typingUsers - .filter((user: { user: object; userId: string; }) => user?.user && user?.userId !== userId) + .filter((user: { user: object; userId: string; }) => user?.user && user?.userId !== currentUser?.userId) .map((user: { user: object; }) => user.user); return }; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/queries.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-typing-indicator/queries.ts similarity index 96% rename from bigbluebutton-html5/imports/ui/components/chat/chat-graphql/queries.ts rename to bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-typing-indicator/queries.ts index 0f56980a84..2b897d5627 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/queries.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-typing-indicator/queries.ts @@ -8,7 +8,6 @@ export const IS_TYPING_PUBLIC_SUBSCRIPTION = gql`subscription IsTyping($chatId: chatId: {_eq: $chatId} } ) { - meetingId chatId userId typingAt @@ -27,7 +26,6 @@ export const IS_TYPING_PRIVATE_SUBSCRIPTION = gql`subscription IsTyping($chatId: chatId: {_eq: $chatId} } ) { - meetingId chatId userId typingAt diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-typing-indicator/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-typing-indicator/styles.ts index e650c0a5be..93cd12728f 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-typing-indicator/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-typing-indicator/styles.ts @@ -38,31 +38,14 @@ const TypingIndicator = styled.span` `; const TypingIndicatorWrapper = styled.div` - ${({ error }) => error && ` - color: ${colorDanger}; font-size: calc(${fontSizeBase} * .75); color: ${colorGrayDark}; text-align: left; + vertical-align: top; padding: ${borderSize} 0; - position: relative; height: .93rem; max-height: .93rem; - `} - - ${({ info }) => info && ` - font-size: calc(${fontSizeBase} * .75); - color: ${colorGrayDark}; - text-align: left; - padding: ${borderSize} 0; - position: relative; - height: .93rem; - max-height: .93rem; - `} - - ${({ spacer }) => spacer && ` - height: .93rem; - max-height: .93rem; - `} + margin-bottom: .25rem; `; export default { diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/component.tsx new file mode 100644 index 0000000000..56f4df7502 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/component.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import ChatHeader from './chat-header/component'; +import { layoutSelect, layoutSelectInput } from '../../layout/context'; +import { Input, Layout } from '../../layout/layoutTypes'; +import Styled from './styles'; +import ChatMessageListContainer from './chat-message-list/component'; +import ChatMessageFormContainer from './chat-message-form/component'; +import ChatTypingIndicatorContainer from './chat-typing-indicator/component'; +import { PANELS, ACTIONS } from '/imports/ui/components/layout/enums'; +import { CircularProgress } from "@mui/material"; +import usePendingChat from '/imports/ui/core/local-states/usePendingChat'; +import useChat from '/imports/ui/core/hooks/useChat'; +import { Chat } from '/imports/ui/Types/chat'; +import { layoutDispatch } from '/imports/ui/components/layout/context'; +interface ChatProps { + +} + +const Chat: React.FC = () => { + return ( + + + + + + + ); +}; + +const ChatLoading: React.FC = () => { + return + + ; +}; + +const ChatContainer: React.FC = () => { + const idChatOpen = layoutSelect((i: Layout) => i.idChatOpen); + const sidebarContent = layoutSelectInput((i: Input) => i.sidebarContent); + const layoutContextDispatch = layoutDispatch(); + const chats = useChat((chat) => { + return { + chatId: chat.chatId, + participant: chat.participant, + }; + }) as Partial[]; + + const [pendingChat, setPendingChat] = usePendingChat(); + + if (pendingChat) { + const chat = chats.find((c) => { return c.participant?.userId === pendingChat }); + if (chat) { + setPendingChat(''); + layoutContextDispatch({ + type: ACTIONS.SET_ID_CHAT_OPEN, + value: chat.chatId, + }); + } + } + + if (sidebarContent.sidebarContentPanel !== PANELS.CHAT) return null; + if (!idChatOpen) return ; + return ; +}; + +export default ChatContainer; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/styles.ts new file mode 100644 index 0000000000..7b820dcbfb --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/styles.ts @@ -0,0 +1,64 @@ +import styled from 'styled-components'; +import { colorWhite, colorPrimary } from '/imports/ui/stylesheets/styled-components/palette'; +import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints'; +import { mdPaddingX } from '/imports/ui/stylesheets/styled-components/general'; + +export const Chat = styled.div` + background-color: ${colorWhite}; + padding: ${mdPaddingX}; + padding-bottom: 0; + display: flex; + flex-grow: 1; + flex-direction: column; + justify-content: space-around; + overflow: hidden; + height: 100%; + + a { + color: ${colorPrimary}; + text-decoration: none; + + &:focus { + color: ${colorPrimary}; + text-decoration: underline; + } + &:hover { + filter: brightness(90%); + text-decoration: underline; + } + &:active { + filter: brightness(85%); + text-decoration: underline; + } + &:hover:focus { + filter: brightness(90%); + text-decoration: underline; + } + &:focus:active { + filter: brightness(85%); + text-decoration: underline; + } + } + u { + text-decoration-line: none; + } + + ${({ isChrome }) => + isChrome && + ` + transform: translateZ(0); + `} + + @media ${smallOnly} { + transform: none !important; + } +`; + +const ChatContent = styled.div` + height: 100%; + display: contents; +`; + +const ChatMessages = styled.div``; + +export default { Chat, ChatMessages, ChatContent }; diff --git a/bigbluebutton-html5/imports/ui/components/chat/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/container.jsx index 06c4e23a14..46bf52eb5c 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/container.jsx @@ -10,11 +10,13 @@ import { GroupChatContext } from '../components-data/group-chat-context/context' import { UsersContext } from '../components-data/users-context/context'; import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger'; import lockContextContainer from '/imports/ui/components/lock-viewers/context/container'; -import Chat from '/imports/ui/components/chat/component'; +// 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'; +import Chat from './chat-graphql/component'; + const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id; const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id; @@ -249,7 +251,7 @@ const ChatContainer = (props) => { ); }; -export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks }) => { +lockContextContainer(injectIntl(withTracker(({ intl, userLocks }) => { const isChatLockedPublic = userLocks.userPublicChat; const isChatLockedPrivate = userLocks.userPrivateChat; @@ -267,3 +269,5 @@ export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks }) }, }; })(ChatContainer))); + +export default Chat; diff --git a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx index a91bcffae1..3f5c2e17ba 100755 --- a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx @@ -6,10 +6,10 @@ import GroupChat from '/imports/api/group-chat'; import Annotations from '/imports/api/annotations'; import Users from '/imports/api/users'; import { Annotations as AnnotationsLocal } from '/imports/ui/components/whiteboard/service'; -import { - localCollectionRegistry, -} from '/client/collection-mirror-initializer'; -import SubscriptionRegistry, { subscriptionReactivity } from '../../services/subscription-registry/subscriptionRegistry'; +import { localCollectionRegistry } from '/client/collection-mirror-initializer'; +import SubscriptionRegistry, { + subscriptionReactivity, +} from '../../services/subscription-registry/subscriptionRegistry'; import { isChatEnabled } from '/imports/ui/services/features'; const CHAT_CONFIG = Meteor.settings.public.chat; @@ -17,13 +17,39 @@ const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public; const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled; const SUBSCRIPTIONS = [ - 'users', 'meetings', 'polls', 'presentations', 'slides', 'slide-positions', 'captions', - 'voiceUsers', 'whiteboard-multi-user', 'screenshare', 'group-chat', - 'presentation-pods', 'users-settings', 'guestUser', 'users-infos', 'meeting-time-remaining', - 'local-settings', 'users-typing', 'record-meetings', 'video-streams', - 'connection-status', 'voice-call-states', 'external-video-meetings', 'breakouts', 'breakouts-history', - 'pads', 'pads-sessions', 'pads-updates', 'notifications', 'audio-captions', - 'layout-meetings', 'user-reaction', 'timer', + 'users', + 'meetings', + 'polls', + 'presentations', + 'slides', + 'slide-positions', + 'captions', + 'voiceUsers', + 'whiteboard-multi-user', + 'screenshare', + 'presentation-pods', + 'users-settings', + 'guestUser', + 'users-infos', + 'meeting-time-remaining', + 'local-settings', + 'users-typing', + 'record-meetings', + 'video-streams', + 'connection-status', + 'voice-call-states', + 'external-video-meetings', + 'breakouts', + 'breakouts-history', + 'pads', + 'pads-sessions', + 'pads-updates', + 'notifications', + 'audio-captions', + 'layout-meetings', + 'user-reaction', + 'timer', + // 'group-chat' ]; const { localBreakoutsSync, @@ -72,10 +98,13 @@ export default withTracker(() => { const subscriptionErrorHandler = { onError: (error) => { - logger.error({ - logCode: 'startup_client_subscription_error', - extraInfo: { error }, - }, 'Error while subscribing to collections'); + logger.error( + { + logCode: 'startup_client_subscription_error', + extraInfo: { error }, + }, + 'Error while subscribing to collections' + ); Session.set('codeError', error.error); }, }; @@ -84,8 +113,11 @@ export default withTracker(() => { let subscriptionsHandlers = SUBSCRIPTIONS.map((name) => { let subscriptionHandlers = subscriptionErrorHandler; - if ((!TYPING_INDICATOR_ENABLED && name.indexOf('typing') !== -1) - || (!isChatEnabled() && name.indexOf('chat') !== -1)) return null; + if ( + (!TYPING_INDICATOR_ENABLED && name.indexOf('typing') !== -1) || + (!isChatEnabled() && name.indexOf('chat') !== -1) + ) + return null; if (name === 'users') { subscriptionHandlers = { @@ -100,7 +132,7 @@ export default withTracker(() => { return SubscriptionRegistry.createSubscription(name, subscriptionHandlers); }); - if (currentUser && (oldRole !== currentUser?.role)) { + if (currentUser && oldRole !== currentUser?.role) { // stop subscription from the client-side as the server-side only watch moderators if (oldRole === 'VIEWER' && currentUser?.role === 'MODERATOR') { // let this withTracker re-execute when a subscription is stopped @@ -126,17 +158,17 @@ export default withTracker(() => { oldRole = currentUser?.role; } - subscriptionsHandlers = subscriptionsHandlers.filter(obj => obj); - const ready = subscriptionsHandlers.every(handler => handler.ready()); + subscriptionsHandlers = subscriptionsHandlers.filter((obj) => obj); + const ready = subscriptionsHandlers.every((handler) => handler.ready()); let groupChatMessageHandler = {}; - if (isChatEnabled() && ready) { - const subHandler = { - ...subscriptionErrorHandler, - }; + // if (isChatEnabled() && ready) { + // const subHandler = { + // ...subscriptionErrorHandler, + // }; - groupChatMessageHandler = Meteor.subscribe('group-chat-msg', subHandler); - } + // groupChatMessageHandler = Meteor.subscribe('group-chat-msg', subHandler); + // } // TODO: Refactor all the late subscribers let usersPersistentDataHandler = {}; @@ -157,8 +189,8 @@ export default withTracker(() => { ...subscriptionErrorHandler, }); - Object.values(localCollectionRegistry).forEach( - (localCollection) => localCollection.checkForStaleData(), + Object.values(localCollectionRegistry).forEach((localCollection) => + localCollection.checkForStaleData() ); } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/chat-list/chat-list-item/component.tsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/chat-list/chat-list-item/component.tsx index 241138248e..848e935f60 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/chat-list/chat-list-item/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/chat-list/chat-list-item/component.tsx @@ -6,8 +6,8 @@ import { Meteor } from 'meteor/meteor' import Styled from './styles'; import Icon from '/imports/ui/components/common/icon/component'; import { Input, Layout } from '/imports/ui/components/layout/layoutTypes'; -import { Chat } from './chatTypes'; import { UseShortcutHelp, useShortcutHelp } from '/imports/ui/components/shortcut-help/useShortcutHelp' +import { Chat } from '/imports/ui/Types/chat'; const intlMessages = defineMessages({ titlePublic: { @@ -25,7 +25,7 @@ const intlMessages = defineMessages({ }); interface ChatListItemProps { - chat: Chat, + chat: Partial, } const CHAT_CONFIG = Meteor.settings.public.chat; @@ -108,10 +108,9 @@ const ChatListItem = (props: ChatListItemProps) => { ? intl.formatMessage(intlMessages.titlePublic) : chat.participant.name; - const arialabel = `${localizedChatName} ${ - countUnreadMessages > 1 - ? intl.formatMessage(intlMessages.unreadPlural, { 0: countUnreadMessages }) - : intl.formatMessage(intlMessages.unreadSingular)}`; + const arialabel = `${localizedChatName} ${countUnreadMessages > 1 + ? intl.formatMessage(intlMessages.unreadPlural, { 0: countUnreadMessages }) + : intl.formatMessage(intlMessages.unreadSingular)}`; return ( { - const { data } = useSubscription(CHATS_SUBSCRIPTION); +interface ChatListProps { + chats: Partial[], +} - const getActiveChats = () => { - if (data) { - const { chat: chats } = data; +const getActiveChats = (chats: Partial[]) => { + return chats.map((chat) => ( + + + + + + )); +} + +const ChatList: React.FC = ({ chats }) => { - return chats.map( (chat: Chat) => ( - - - - - - )); - } else { - return null; - } - } const intl = useIntl(); return ( - - - - {intl.formatMessage(intlMessages.messagesTitle)} - - - - - - {getActiveChats()} - - - - ) + + + + {intl.formatMessage(intlMessages.messagesTitle)} + + + + + + {getActiveChats(chats) ?? null} + + + + ) }; -export default ChatList; +const ChatListContainer: React.FC = () => { + const chats = useChat((chat) => { return chat; }) as Partial[]; + return ( + + ); +}; + +export default ChatListContainer; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx index 79999056fd..ef05b2fefd 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { User } from '/imports/ui/Types/user'; -import {LockSettings, UsersPolicies} from '/imports/ui/Types/meeting'; +import { LockSettings, UsersPolicies } from '/imports/ui/Types/meeting'; import { generateActionsPermissions, isVoiceOnlyUser } from './service'; import { useIntl, defineMessages } from 'react-intl'; import { @@ -22,6 +22,7 @@ import { EMOJI_STATUSES } from '/imports/utils/statuses'; import ConfirmationModal from '/imports/ui/components/common/modal/confirmation/component'; import BBBMenu from '/imports/ui/components/common/menu/component'; +import { setPendingChat } from '/imports/ui/core/local-states/usePendingChat'; interface UserActionsProps { user: User; @@ -131,7 +132,6 @@ const UserActions: React.FC = ({ usersPolicies, isBreakout ); - const { allowedToChangeStatus, allowedToChatPrivately, @@ -151,7 +151,7 @@ const UserActions: React.FC = ({ const { disablePrivateChat, } = lockSettings; - + const userLocked = user.locked && lockSettings.hasActiveLockSetting && user.isModerator; @@ -183,17 +183,18 @@ const UserActions: React.FC = ({ allowed: isChatEnabled() && ( currentUser.isModerator ? allowedToChatPrivately - : allowedToChatPrivately && ( - !(currentUser.locked && disablePrivateChat) - // TODO: Add check for hasPrivateChat between users - || user.isModerator - ) + : allowedToChatPrivately && ( + !(currentUser.locked && disablePrivateChat) + // TODO: Add check for hasPrivateChat between users + || user.isModerator + ) ) && !isVoiceOnlyUser(user.userId) && !isBreakout, key: 'activeChat', label: intl.formatMessage(messages.StartPrivateChat), onClick: () => { + setPendingChat(user.userId); setSelected(false); sendCreatePrivateChat(user); layoutContextDispatch({ @@ -206,7 +207,7 @@ const UserActions: React.FC = ({ }); layoutContextDispatch({ type: ACTIONS.SET_ID_CHAT_OPEN, - value: user.userId, + value: '', }); }, icon: 'chat', @@ -366,53 +367,53 @@ const UserActions: React.FC = ({ })), ]; - const actions = showNestedOptions + const actions = showNestedOptions ? nestedOptions.filter(key => key.allowed) : dropdownOptions.filter(key => key.allowed); if (!actions.length) return children; return
- setSelected(true)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - setSelected(true); - } - }} - role="button" - > - {children} -
- ) - } - actions={actions} - selectedEmoji={user.emoji} - onCloseCallback={() =>{ - setSelected(false); - setShowNestedOptions(false); - }} - open={selected} -/> -{isConfirmationModalOpen ? setIsConfirmationModalOpen(false), - priority: "low", - setIsOpen: setIsConfirmationModalOpen, - isOpen: isConfirmationModalOpen - }} -/> : null} + setSelected(true)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + setSelected(true); + } + }} + role="button" + > + {children} +
+ ) + } + actions={actions} + selectedEmoji={user.emoji} + onCloseCallback={() => { + setSelected(false); + setShowNestedOptions(false); + }} + open={selected} + /> + {isConfirmationModalOpen ? setIsConfirmationModalOpen(false), + priority: "low", + setIsOpen: setIsConfirmationModalOpen, + isOpen: isConfirmationModalOpen + }} + /> : null}
; }; diff --git a/bigbluebutton-html5/imports/ui/core/events/events.ts b/bigbluebutton-html5/imports/ui/core/events/events.ts new file mode 100644 index 0000000000..b0aa306f2d --- /dev/null +++ b/bigbluebutton-html5/imports/ui/core/events/events.ts @@ -0,0 +1,7 @@ + +const enum Events { + SENT_MESSAGE = 'sentMessage', + RESTORE_WELCOME_MESSAGES = 'restoreWelcomeMessages', +} + +export default Events; diff --git a/bigbluebutton-html5/imports/ui/core/graphql/queries/chatSubscription.ts b/bigbluebutton-html5/imports/ui/core/graphql/queries/chatSubscription.ts new file mode 100644 index 0000000000..5239c37aa4 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/core/graphql/queries/chatSubscription.ts @@ -0,0 +1,29 @@ +import { gql } from '@apollo/client'; + +export const CHATS_SUBSCRIPTION = gql` + subscription { + chat( + order_by: [ + { public: desc } + { totalUnread: desc } + { participant: { name: asc, userId: asc } } + ] + ) { + chatId + participant { + userId + name + role + color + loggedOut + avatar + isOnline + isModerator + } + totalMessages + totalUnread + public + lastSeenAt + } + } +`; diff --git a/bigbluebutton-html5/imports/ui/core/graphql/queries/meetingSubscription.ts b/bigbluebutton-html5/imports/ui/core/graphql/queries/meetingSubscription.ts new file mode 100644 index 0000000000..14908cc749 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/core/graphql/queries/meetingSubscription.ts @@ -0,0 +1,78 @@ +import { gql } from "@apollo/client"; + +export const MEETING_SUBSCRIPTION = gql` + subscription MeetingSubscription { + meeting { + createdTime + disabledFeatures + duration + extId + lockSettings { + disableCam + disableMic + disableNotes + disablePrivateChat + disablePublicChat + hasActiveLockSetting + hideUserList + hideViewersCursor + webcamsOnlyForModerator + } + maxPinnedCameras + meetingCameraCap + meetingId + name + notifyRecordingIsOn + presentationUploadExternalDescription + presentationUploadExternalUrl + recordingPolicies { + allowStartStopRecording + autoStartRecording + record + keepEvents + } + screenshare { + hasAudio + screenshareId + stream + vidHeight + vidWidth + voiceConf + screenshareConf + } + usersPolicies { + allowModsToEjectCameras + allowModsToUnmuteUsers + authenticatedGuest + guestPolicy + maxUserConcurrentAccesses + maxUsers + meetingLayout + moderatorsCanMuteAudio + moderatorsCanUnmuteAudio + userCameraCap + webcamsOnlyForModerator + } + isBreakout + breakoutPolicies { + breakoutRooms + captureNotes + captureNotesFilename + captureSlides + captureSlidesFilename + freeJoin + parentId + privateChatEnabled + record + sequence + } + html5InstanceId + voiceSettings { + dialNumber + muteOnStart + voiceConf + telVoice + } + } + } +`; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/core/hooks/useChat.ts b/bigbluebutton-html5/imports/ui/core/hooks/useChat.ts new file mode 100644 index 0000000000..c7d086b7c9 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/core/hooks/useChat.ts @@ -0,0 +1,17 @@ +import { createUseSubsciption } from "./createUseSubscription"; +import { CHATS_SUBSCRIPTION } from "../graphql/queries/chatSubscription"; +import { Chat } from "../../Types/chat"; + +const useChatSubscription = createUseSubsciption>(CHATS_SUBSCRIPTION); + +const useChat = (fn: (c: Partial)=> Partial, chatId?: string ): Array> | Partial | null =>{ + const chats = useChatSubscription(fn); + if (chatId) { + return chats.find((c) => { + return c.chatId === chatId + }) ?? null; + } + return chats; +}; + +export default useChat; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/core/hooks/useCurrentUser.ts b/bigbluebutton-html5/imports/ui/core/hooks/useCurrentUser.ts index 29106b08f7..bf67168421 100644 --- a/bigbluebutton-html5/imports/ui/core/hooks/useCurrentUser.ts +++ b/bigbluebutton-html5/imports/ui/core/hooks/useCurrentUser.ts @@ -5,7 +5,7 @@ import { User } from "../../Types/user"; const useCurrentUserSubscription = createUseSubsciption>(CURRENT_USER_SUBSCRIPTION, false); -export const useCurrentUser = (fn: (c: Partial)=> Array>)=>{ +export const useCurrentUser = (fn: (c: Partial)=> Partial)=>{ const currentUser = useCurrentUserSubscription(fn)[0]; return currentUser; }; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/core/hooks/useMeeting.ts b/bigbluebutton-html5/imports/ui/core/hooks/useMeeting.ts new file mode 100644 index 0000000000..db5f3a242b --- /dev/null +++ b/bigbluebutton-html5/imports/ui/core/hooks/useMeeting.ts @@ -0,0 +1,11 @@ +import { createUseSubsciption } from "./createUseSubscription"; +import { MEETING_SUBSCRIPTION } from "../graphql/queries/meetingSubscription"; +import { Meeting } from "../../Types/meeting"; + + +const useMeetingSubscription = createUseSubsciption>(MEETING_SUBSCRIPTION, false); + +export const useMeeting = (fn: (c: Partial)=> Partial): Partial=>{ + const meeting = useMeetingSubscription(fn)[0]; + return meeting; +}; \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/core/local-states/createUseLocalState.ts b/bigbluebutton-html5/imports/ui/core/local-states/createUseLocalState.ts new file mode 100644 index 0000000000..a6badae70e --- /dev/null +++ b/bigbluebutton-html5/imports/ui/core/local-states/createUseLocalState.ts @@ -0,0 +1,21 @@ +import { makeVar, useReactiveVar } from '@apollo/client'; + +function createUseLocalState(initialValue: T) { + const localState = makeVar(initialValue); + + function useLocalState(): [T, (value: T) => void] { + const reactiveLocalState = useReactiveVar(localState); + return [reactiveLocalState, changeLocalState]; + } + + function changeLocalState(value: T | Function) { + if (typeof value === 'function') { + return localState(value(localState())); + } + return localState(value); + } + + return [useLocalState, changeLocalState, localState]; +} + +export default createUseLocalState; diff --git a/bigbluebutton-html5/imports/ui/core/local-states/usePendingChat.ts b/bigbluebutton-html5/imports/ui/core/local-states/usePendingChat.ts new file mode 100644 index 0000000000..d195d4679d --- /dev/null +++ b/bigbluebutton-html5/imports/ui/core/local-states/usePendingChat.ts @@ -0,0 +1,7 @@ +import createUseLocalState from './createUseLocalState'; + +const initialPendingChat: string = ''; +const [usePendingChat, setPendingChat] = createUseLocalState(initialPendingChat); + +export default usePendingChat; +export { setPendingChat }; diff --git a/bigbluebutton-html5/imports/utils/throttle.js b/bigbluebutton-html5/imports/utils/throttle.js index 542120f00d..4f1f70c466 100644 --- a/bigbluebutton-html5/imports/utils/throttle.js +++ b/bigbluebutton-html5/imports/utils/throttle.js @@ -1,28 +1,35 @@ export function throttle(func, delay, options = {}) { + let lastInvocation = 0; + let isWaiting = false; let timeoutId; - let lastExecTime = 0; - let leadingExec = true; - const { leading = true, trailing = true } = options; + const leading = options.leading !== undefined ? options.leading : true; + const trailing = options.trailing !== undefined ? options.trailing : true; - return function () { - const context = this; - const args = arguments; - const elapsed = Date.now() - lastExecTime; + return function throttled(...args) { + const invokeFunction = () => { + lastInvocation = Date.now(); + isWaiting = false; + func.apply(this, args); + }; - function execute() { - func.apply(context, args); - lastExecTime = Date.now(); + if (!isWaiting) { + if (leading) { + invokeFunction(); + } else { + isWaiting = true; + } + + const currentTime = Date.now(); + const timeSinceLastInvocation = currentTime - lastInvocation; + + if (timeSinceLastInvocation >= delay) { + clearTimeout(timeoutId); + invokeFunction(); + } else if (trailing) { + clearTimeout(timeoutId); + timeoutId = setTimeout(invokeFunction, delay - timeSinceLastInvocation); + } } - - if (leadingExec && leading) { - execute(); - leadingExec = false; - } else if (!timeoutId && trailing) { - timeoutId = setTimeout(function () { - execute(); - timeoutId = null; - }, delay - elapsed); - } - } -} + }; +} \ No newline at end of file diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index 7d840b92bf..bf697902d7 100644 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -84,6 +84,7 @@ "react-toggle": "^4.1.2", "react-transition-group": "^2.9.0", "react-virtualized": "^9.22.4", + "recharts": "^2.7.2", "reconnecting-websocket": "~v4.4.0", "redis": "^3.1.2", "sanitize-html": "2.7.1", diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index cdc5465a33..ed1106a3e5 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -1,1370 +1,1371 @@ { - "app.home.greeting": "Your presentation will begin shortly ...", - "app.chat.submitLabel": "Send message", - "app.chat.loading": "Chat messages loaded: {0}%", - "app.chat.errorMaxMessageLength": "The message is too long, exceeded the maximum of {0} characters", - "app.chat.disconnected": "You are disconnected, messages can't be sent", - "app.chat.locked": "Chat is locked, messages can't be sent", - "app.chat.inputLabel": "Message input for chat {0}", - "app.chat.emojiButtonLabel": "Emoji Picker", - "app.chat.loadMoreButtonLabel": "Load More", - "app.chat.inputPlaceholder": "Message {0}", - "app.chat.titlePublic": "Public Chat", - "app.chat.titlePrivate": "Private Chat with {0}", - "app.chat.partnerDisconnected": "{0} has left the meeting", - "app.chat.closeChatLabel": "Close {0}", - "app.chat.hideChatLabel": "Hide {0}", - "app.chat.moreMessages": "More messages below", - "app.chat.dropdown.options": "Chat options", - "app.chat.dropdown.clear": "Clear", - "app.chat.dropdown.copy": "Copy", - "app.chat.dropdown.save": "Save", - "app.chat.label": "Chat", - "app.chat.offline": "Offline", - "app.chat.pollResult": "Poll Results", - "app.chat.breakoutDurationUpdated": "Breakout time is now {0} minutes", - "app.chat.breakoutDurationUpdatedModerator": "Breakout rooms time is now {0} minutes, and a notification has been sent.", - "app.chat.emptyLogLabel": "Chat log empty", - "app.chat.away": "Is away", - "app.chat.notAway": "Is not away anymore", - "app.chat.clearPublicChatMessage": "The public chat history was cleared by a moderator", - "app.chat.multi.typing": "Multiple users are typing", - "app.chat.one.typing": "{0} is typing", - "app.chat.two.typing": "{0} and {1} are typing", - "app.chat.copySuccess": "Copied chat transcript", - "app.chat.copyErr": "Copy chat transcript failed", - "app.emojiPicker.search": "Search", - "app.emojiPicker.notFound": "No Emoji Found", - "app.emojiPicker.skintext": "Choose your default skin tone", - "app.emojiPicker.clear": "Clear", - "app.emojiPicker.categories.label": "Emoji categories", - "app.emojiPicker.categories.people": "People & Body", - "app.emojiPicker.categories.reactions": "Reactions", - "app.emojiPicker.categories.nature": "Animals & Nature", - "app.emojiPicker.categories.foods": "Food & Drink", - "app.emojiPicker.categories.places": "Travel & Places", - "app.emojiPicker.categories.activity": "Activity", - "app.emojiPicker.categories.objects": "Objects", - "app.emojiPicker.categories.symbols": "Symbols", - "app.emojiPicker.categories.flags": "Flags", - "app.emojiPicker.categories.recent": "Frequently Used", - "app.emojiPicker.categories.search": "Search Results", - "app.emojiPicker.skintones.1": "Default Skin Tone", - "app.emojiPicker.skintones.2": "Light Skin Tone", - "app.emojiPicker.skintones.3": "Medium-Light Skin Tone", - "app.emojiPicker.skintones.4": "Medium Skin Tone", - "app.emojiPicker.skintones.5": "Medium-Dark Skin Tone", - "app.emojiPicker.skintones.6": "Dark Skin Tone", - "app.timer.title": "Time", - "app.timer.stopwatch.title": "Stopwatch", - "app.timer.timer.title": "Timer", - "app.timer.hideTimerLabel": "Hide time", - "app.timer.button.stopwatch": "Stopwatch", - "app.timer.button.timer": "Timer", - "app.timer.button.start": "Start", - "app.timer.button.stop": "Stop", - "app.timer.button.reset": "Reset", - "app.timer.hours": "hours", - "app.timer.minutes": "minutes", - "app.timer.seconds": "seconds", - "app.timer.songs": "Songs", - "app.timer.noTrack": "No song", - "app.timer.track1": "Relaxing", - "app.timer.track2": "Calm", - "app.timer.track3": "Happy", - "app.captions.label": "Captions", - "app.captions.menu.close": "Close", - "app.captions.menu.start": "Start", - "app.captions.menu.ariaStart": "Start writing captions", - "app.captions.menu.ariaStartDesc": "Opens captions editor and closes the modal", - "app.captions.menu.select": "Select available language", - "app.captions.menu.ariaSelect": "Captions language", - "app.captions.menu.subtitle": "Please select a language and styles for closed captions within your session.", - "app.captions.menu.title": "Closed captions", - "app.captions.menu.fontSize": "Size", - "app.captions.menu.fontColor": "Text color", - "app.captions.menu.fontFamily": "Font", - "app.captions.menu.backgroundColor": "Background color", - "app.captions.menu.previewLabel": "Preview", - "app.captions.menu.cancelLabel": "Cancel", - "app.captions.hide": "Hide closed captions", - "app.captions.ownership": "Take over", - "app.captions.ownershipTooltip": "You will be assigned as the owner of {0} captions", - "app.captions.dictationStart": "Start dictation", - "app.captions.dictationStop": "Stop dictation", - "app.captions.dictationOnDesc": "Turns speech recognition on", - "app.captions.dictationOffDesc": "Turns speech recognition off", - "app.captions.speech.start": "Speech recognition started", - "app.captions.speech.stop": "Speech recognition stopped", - "app.captions.speech.error": "Speech recognition stopped due to the browser incompatibility or some time of silence", - "app.confirmation.skipConfirm": "Don't ask again", - "app.confirmation.virtualBackground.title": "Start new virtual background", - "app.confirmation.virtualBackground.description": "{0} will be added as virtual background. Continue?", - "app.confirmationModal.yesLabel": "Yes", - "app.textInput.sendLabel": "Send", - "app.title.defaultViewLabel": "Default presentation view", - "app.notes.title": "Shared Notes", - "app.notes.titlePinned": "Shared Notes (Pinned)", - "app.notes.pinnedNotification": "The Shared Notes are now pinned in the whiteboard.", - "app.notes.label": "Notes", - "app.notes.hide": "Hide notes", - "app.notes.locked": "Locked", - "app.notes.disabled": "Pinned on media area", - "app.notes.notesDropdown.covertAndUpload": "Convert notes to presentation", - "app.notes.notesDropdown.pinNotes": "Pin notes onto whiteboard", - "app.notes.notesDropdown.unpinNotes": "Unpin notes", - "app.notes.notesDropdown.notesOptions": "Notes options", - "app.pads.hint": "Press Esc to focus the pad's toolbar", - "app.user.activityCheck": "User activity check", - "app.user.activityCheck.label": "Check if user is still in meeting ({0})", - "app.user.activityCheck.check": "Check", - "app.userList.usersTitle": "Users", - "app.userList.participantsTitle": "Participants", - "app.userList.messagesTitle": "Messages", - "app.userList.notesTitle": "Notes", - "app.userList.notesListItem.unreadContent": "New content is available in the shared notes section", - "app.userList.timerTitle": "Time", - "app.userList.captionsTitle": "Captions", - "app.userList.presenter": "Presenter", - "app.userList.you": "You", - "app.userList.locked": "Locked", - "app.userList.byModerator": "by (Moderator)", - "app.userList.label": "User list", - "app.userList.toggleCompactView.label": "Toggle compact view mode", - "app.userList.moderator": "Moderator", - "app.userList.mobile": "Mobile", - "app.userList.guest": "Guest", - "app.userList.sharingWebcam": "Webcam", - "app.userList.menuTitleContext": "Available options", - "app.userList.chatListItem.unreadSingular": "One new message", - "app.userList.chatListItem.unreadPlural": "{0} new messages", - "app.userList.menu.away": "Set yourself as away", - "app.userList.menu.notAway": "Set yourself as active", - "app.userList.menu.chat.label": "Start a private chat", - "app.userList.menu.clearStatus.label": "Clear status", - "app.userList.menu.removeUser.label": "Remove user", - "app.userList.menu.removeConfirmation.label": "Remove user ({0})", - "app.userlist.menu.removeConfirmation.desc": "Prevent this user from rejoining the session.", - "app.userList.menu.muteUserAudio.label": "Mute user", - "app.userList.menu.unmuteUserAudio.label": "Unmute user", - "app.userList.menu.webcamPin.label": "Pin user's webcam", - "app.userList.menu.webcamUnpin.label": "Unpin user's webcam", - "app.userList.menu.giveWhiteboardAccess.label" : "Give whiteboard access", - "app.userList.menu.removeWhiteboardAccess.label": "Remove whiteboard access", - "app.userList.menu.ejectUserCameras.label": "Close cameras", - "app.userList.userAriaLabel": "{0} {1} {2} Status {3}", - "app.userList.menu.promoteUser.label": "Promote to moderator", - "app.userList.menu.demoteUser.label": "Demote to viewer", - "app.userList.menu.unlockUser.label": "Unlock {0}", - "app.userList.menu.lockUser.label": "Lock {0}", - "app.userList.menu.directoryLookup.label": "Directory Lookup", - "app.userList.menu.makePresenter.label": "Make presenter", - "app.userList.userOptions.manageUsersLabel": "Manage users", - "app.userList.userOptions.muteAllLabel": "Mute all users", - "app.userList.userOptions.muteAllDesc": "Mutes all users in the meeting", - "app.userList.userOptions.clearAllLabel": "Clear all status icons", - "app.userList.userOptions.clearAllDesc": "Clears all status icons from users", - "app.userList.userOptions.muteAllExceptPresenterLabel": "Mute all users except presenter", - "app.userList.userOptions.muteAllExceptPresenterDesc": "Mutes all users in the meeting except the presenter", - "app.userList.userOptions.unmuteAllLabel": "Turn off meeting mute", - "app.userList.userOptions.unmuteAllDesc": "Unmutes the meeting", - "app.userList.userOptions.lockViewersLabel": "Lock viewers", - "app.userList.userOptions.lockViewersDesc": "Lock certain functionalities for attendees of the meeting", - "app.userList.userOptions.guestPolicyLabel": "Guest policy", - "app.userList.userOptions.guestPolicyDesc": "Change meeting guest policy setting", - "app.userList.userOptions.disableCam": "Viewers' webcams are disabled", - "app.userList.userOptions.disableMic": "Viewers' microphones are disabled", - "app.userList.userOptions.disablePrivChat": "Private chat is disabled", - "app.userList.userOptions.disablePubChat": "Public chat is disabled", - "app.userList.userOptions.disableNotes": "Shared notes are now locked", - "app.userList.userOptions.hideUserList": "User list is now hidden for viewers", - "app.userList.userOptions.webcamsOnlyForModerator": "Only moderators are able to see viewers' webcams (due to lock settings)", - "app.userList.content.participants.options.clearedStatus": "Cleared all user status", - "app.userList.userOptions.enableCam": "Viewers' webcams are enabled", - "app.userList.userOptions.enableMic": "Viewers' microphones are enabled", - "app.userList.userOptions.enablePrivChat": "Private chat is enabled", - "app.userList.userOptions.enablePubChat": "Public chat is enabled", - "app.userList.userOptions.enableNotes": "Shared notes are now enabled", - "app.userList.userOptions.showUserList": "User list is now shown to viewers", - "app.userList.userOptions.enableOnlyModeratorWebcam": "You can enable your webcam now, everyone will see you", - "app.userList.userOptions.savedNames.title": "List of users in meeting {0} at {1}", - "app.userList.userOptions.sortedFirstName.heading": "Sorted by first name:", - "app.userList.userOptions.sortedLastName.heading": "Sorted by last name:", - "app.userList.userOptions.hideViewersCursor": "Viewer cursors are locked", - "app.userList.userOptions.showViewersCursor": "Viewer cursors are unlocked", - "app.media.label": "Media", - "app.media.autoplayAlertDesc": "Allow Access", - "app.media.screenshare.start": "Screenshare has started", - "app.media.screenshare.end": "Screenshare has ended", - "app.media.screenshare.endDueToDataSaving": "Screenshare stopped due to data savings", - "app.media.screenshare.unavailable": "Screenshare Unavailable", - "app.media.screenshare.notSupported": "Screensharing is not supported in this browser.", - "app.media.screenshare.autoplayBlockedDesc": "We need your permission to show you the presenter's screen.", - "app.media.screenshare.autoplayAllowLabel": "View shared screen", - "app.media.cameraAsContent.start": "Present camera has started", - "app.media.cameraAsContent.end": "Present camera has ended", - "app.media.cameraAsContent.endDueToDataSaving": "Present camera stopped due to data savings", - "app.media.cameraAsContent.autoplayBlockedDesc": "We need your permission to show you the presenter's camera.", - "app.media.cameraAsContent.autoplayAllowLabel": "View present camera", - "app.screenshare.presenterLoadingLabel": "Your screenshare is loading", - "app.screenshare.viewerLoadingLabel": "The presenter's screen is loading", - "app.screenshare.presenterSharingLabel": "You are now sharing your screen", - "app.screenshare.screenshareFinalError": "Code {0}. Could not share the screen.", - "app.screenshare.screenshareRetryError": "Code {0}. Try sharing the screen again.", - "app.screenshare.screenshareRetryOtherEnvError": "Code {0}. Could not share the screen. Try again using a different browser or device.", - "app.screenshare.screenshareUnsupportedEnv": "Code {0}. Browser is not supported. Try again using a different browser or device.", - "app.screenshare.screensharePermissionError": "Code {0}. Permission to capture the screen needs to be granted.", - "app.cameraAsContent.presenterLoadingLabel": "Your camera is loading", - "app.cameraAsContent.viewerLoadingLabel": "The presenter's camera is loading", - "app.cameraAsContent.presenterSharingLabel": "You are now presenting your camera", - "app.meeting.ended": "This session has ended", - "app.meeting.meetingTimeRemaining": "Meeting time remaining: {0}", - "app.meeting.meetingTimeHasEnded": "Time ended. Meeting will close soon", - "app.meeting.endedByUserMessage": "This session was ended by {0}", - "app.meeting.endedByNoModeratorMessageSingular": "The meeting has ended because no moderator has been present for one minute", - "app.meeting.endedByNoModeratorMessagePlural": "The meeting has ended because no moderator has been present for {0} minutes", - "app.meeting.endedMessage": "You will be forwarded back to the home screen", - "app.meeting.alertMeetingEndsUnderMinutesSingular": "Meeting is closing in one minute.", - "app.meeting.alertMeetingEndsUnderMinutesPlural": "Meeting is closing in {0} minutes.", - "app.meeting.alertBreakoutEndsUnderMinutesPlural": "Breakout is closing in {0} minutes.", - "app.meeting.alertBreakoutEndsUnderMinutesSingular": "Breakout is closing in one minute.", - "app.presentation.hide": "Hide presentation", - "app.presentation.notificationLabel": "Current presentation", - "app.presentation.downloadLabel": "Download", - "app.presentation.slideContent": "Slide Content", - "app.presentation.startSlideContent": "Slide content start", - "app.presentation.endSlideContent": "Slide content end", - "app.presentation.changedSlideContent": "Presentation changed to slide: {0}", - "app.presentation.emptySlideContent": "No content for current slide", - "app.presentation.options.fullscreen": "Fullscreen Presentation", - "app.presentation.options.exitFullscreen": "Exit Fullscreen", - "app.presentation.options.minimize": "Minimize", - "app.presentation.options.snapshot": "Snapshot of current slide", - "app.presentation.options.downloading": "Downloading...", - "app.presentation.options.downloaded": "Current presentation was downloaded", - "app.presentation.options.downloadFailed": "Could not download current presentation", - "app.presentation.presentationToolbar.noNextSlideDesc": "End of presentation", - "app.presentation.presentationToolbar.noPrevSlideDesc": "Start of presentation", - "app.presentation.presentationToolbar.selectLabel": "Select slide", - "app.presentation.presentationToolbar.prevSlideLabel": "Previous slide", - "app.presentation.presentationToolbar.prevSlideDesc": "Change the presentation to the previous slide", - "app.presentation.presentationToolbar.nextSlideLabel": "Next slide", - "app.presentation.presentationToolbar.nextSlideDesc": "Change the presentation to the next slide", - "app.presentation.presentationToolbar.skipSlideLabel": "Skip slide", - "app.presentation.presentationToolbar.skipSlideDesc": "Change the presentation to a specific slide", - "app.presentation.presentationToolbar.fitWidthLabel": "Fit to width", - "app.presentation.presentationToolbar.fitWidthDesc": "Display the whole width of the slide", - "app.presentation.presentationToolbar.fitScreenLabel": "Fit to screen", - "app.presentation.presentationToolbar.fitScreenDesc": "Display the whole slide", - "app.presentation.presentationToolbar.zoomLabel": "Zoom", - "app.presentation.presentationToolbar.zoomDesc": "Change the zoom level of the presentation", - "app.presentation.presentationToolbar.zoomInLabel": "Zoom in", - "app.presentation.presentationToolbar.zoomInDesc": "Zoom in the presentation", - "app.presentation.presentationToolbar.zoomOutLabel": "Zoom out", - "app.presentation.presentationToolbar.zoomOutDesc": "Zoom out of the presentation", - "app.presentation.presentationToolbar.zoomReset": "Reset Zoom", - "app.presentation.presentationToolbar.zoomIndicator": "Current zoom percentage", - "app.presentation.presentationToolbar.fitToWidth": "Fit to width", - "app.presentation.presentationToolbar.fitToPage": "Fit to page", - "app.presentation.presentationToolbar.goToSlide": "Slide {0}", - "app.presentation.presentationToolbar.hideToolsDesc": "Hide Toolbars", - "app.presentation.presentationToolbar.showToolsDesc": "Show Toolbars", - "app.presentation.placeholder": "There is no currently active presentation", - "app.presentationUploder.title": "Presentation", - "app.presentationUploder.message": "As a presenter you have the ability to upload any Office document or PDF file. We recommend PDF file for best results. Please ensure that a presentation is selected using the circle checkbox on the left hand side.", - "app.presentationUploader.exportHint": "Selecting \"Send to chat\" will provide users with a downloadable link with annotations in public chat.", - "app.presentationUploader.exportToastHeader": "Sending to chat ({0} item)", - "app.presentationUploader.exportToastHeaderPlural": "Sending to chat ({0} items)", - "app.presentationUploader.exporting": "Sending to chat", - "app.presentationUploader.sending": "Sending...", - "app.presentationUploader.collecting": "Extracting slide {0} of {1}...", - "app.presentationUploader.processing": "Annotating slide {0} of {1}...", - "app.presentationUploader.sent": "Sent", - "app.presentationUploader.exportingTimeout": "The export is taking too long...", - "app.presentationUploader.export": "Send to chat", - "app.presentationUploader.exportCurrentStatePresentation": "Send out a download link for the presentation in the current state of it", - "app.presentationUploader.enableOriginalPresentationDownload": "Enable download of the original presentation", - "app.presentationUploader.disableOriginalPresentationDownload": "Disable download of the original presentation", - "app.presentationUploader.dropdownExportOptions": "Export options", - "app.presentationUploader.export.linkAvailable": "Link for downloading {0} available on the public chat.", - "app.presentationUploader.export.downloadButtonAvailable": "Download button for presentation {0} is available.", - "app.presentationUploader.export.notAccessibleWarning": "may not be accessibility compliant", - "app.presentationUploader.export.originalLabel": "Original", - "app.presentationUploader.export.inCurrentStateLabel": "In current state", - "app.presentationUploader.currentPresentationLabel": "Current presentation", - "app.presentationUploder.extraHint": "IMPORTANT: each file may not exceed {0} MB and {1} pages.", - "app.presentationUploder.uploadLabel": "Upload", - "app.presentationUploder.confirmLabel": "Confirm", - "app.presentationUploder.confirmDesc": "Save your changes and start the presentation", - "app.presentationUploder.dismissLabel": "Cancel", - "app.presentationUploder.dismissDesc": "Close the modal window and discard your changes", - "app.presentationUploder.dropzoneLabel": "Drag files here to upload", - "app.presentationUploder.dropzoneImagesLabel": "Drag images here to upload", - "app.presentationUploder.browseFilesLabel": "or browse for files", - "app.presentationUploder.browseImagesLabel": "or browse/capture for images", - "app.presentationUploder.externalUploadTitle": "Add content from 3rd party application", - "app.presentationUploder.externalUploadLabel": "Browse files", - "app.presentationUploder.fileToUpload": "To be uploaded ...", - "app.presentationUploder.currentBadge": "Current", - "app.presentationUploder.rejectedError": "The selected file(s) have been rejected. Please check the file type(s).", - "app.presentationUploder.connectionClosedError": "Interrupted by poor connectivity. Please try again.", - "app.presentationUploder.upload.progress": "Uploading ({0}%)", - "app.presentationUploder.conversion.204": "No content to capture", - "app.presentationUploder.upload.413": "File is too large, exceeded the maximum of {0} MB", - "app.presentationUploder.genericError": "Oops, Something went wrong ...", - "app.presentationUploder.upload.408": "Request upload token timeout.", - "app.presentationUploder.upload.404": "404: Invalid upload token", - "app.presentationUploder.upload.401": "Request presentation upload token failed.", - "app.presentationUploder.conversion.conversionProcessingSlides": "Processing page {0} of {1}", - "app.presentationUploder.conversion.genericConversionStatus": "Converting file ...", - "app.presentationUploder.conversion.generatingThumbnail": "Generating thumbnails ...", - "app.presentationUploder.conversion.generatedSlides": "Slides generated ...", - "app.presentationUploder.conversion.generatingSvg": "Generating SVG images ...", - "app.presentationUploder.conversion.pageCountExceeded": "Number of pages exceeded maximum of {0}", - "app.presentationUploder.conversion.invalidMimeType": "Invalid format detected (extension={0}, content type={1})", - "app.presentationUploder.conversion.conversionTimeout": "Slide {0} could not be processed within {1} attempts.", - "app.presentationUploder.conversion.officeDocConversionInvalid": "Failed to process Office document. Please upload a PDF instead.", - "app.presentationUploder.conversion.officeDocConversionFailed": "Failed to process Office document. Please upload a PDF instead.", - "app.presentationUploder.conversion.pdfHasBigPage": "We could not convert the PDF file, please try optimizing it. Max page size {0}", - "app.presentationUploder.conversion.timeout": "Ops, the conversion took too long", - "app.presentationUploder.conversion.pageCountFailed": "Failed to determine the number of pages.", - "app.presentationUploder.conversion.unsupportedDocument": "File extension not supported", - "app.presentationUploder.removePresentationLabel": "Remove presentation", - "app.presentationUploder.setAsCurrentPresentation": "Set presentation as current", - "app.presentationUploder.tableHeading.filename": "File name", - "app.presentationUploder.tableHeading.options": "Options", - "app.presentationUploder.tableHeading.status": "Status", - "app.presentationUploder.uploading": "Uploading {0} {1}", - "app.presentationUploder.uploadStatus": "{0} of {1} uploads complete", - "app.presentationUploder.completed": "{0} uploads complete", - "app.presentationUploder.item" : "item", - "app.presentationUploder.itemPlural" : "items", - "app.presentationUploder.clearErrors": "Clear errors", - "app.presentationUploder.clearErrorsDesc": "Clears failed presentation uploads", - "app.presentationUploder.uploadViewTitle": "Upload Presentation", - "app.poll.questionAndoptions.label" : "Question text to be shown.\nA. Poll option *\nB. Poll option (optional)\nC. Poll option (optional)\nD. Poll option (optional)\nE. Poll option (optional)", - "app.poll.customInput.label": "Custom Input", - "app.poll.customInputInstructions.label": "Custom input is enabled – write poll question and option(s) in given format or drag and drop a text file in same format.", - "app.poll.maxOptionsWarning.label": "Only first 5 options can be used!", - "app.poll.pollPaneTitle": "Polling", - "app.poll.enableMultipleResponseLabel": "Allow multiple answers per respondent?", - "app.poll.quickPollTitle": "Quick Poll", - "app.poll.hidePollDesc": "Hides the poll menu pane", - "app.poll.quickPollInstruction": "Select an option below to start your poll.", - "app.poll.activePollInstruction": "Leave this panel open to see live responses to your poll. When you are ready, select 'Publish polling results' to publish the results and end the poll.", - "app.poll.dragDropPollInstruction": "To fill the poll values, drag a text file with the poll values onto the highlighted field", - "app.poll.customPollTextArea": "Fill poll values", - "app.poll.publishLabel": "Publish poll", - "app.poll.cancelPollLabel": "Cancel", - "app.poll.backLabel": "Start A Poll", - "app.poll.closeLabel": "Close", - "app.poll.waitingLabel": "Waiting for responses ({0}/{1})", - "app.poll.ariaInputCount": "Custom poll option {0} of {1}", - "app.poll.customPlaceholder": "Add poll option", - "app.poll.noPresentationSelected": "No presentation selected! Please select one.", - "app.poll.clickHereToSelect": "Click here to select", - "app.poll.question.label" : "Write your question...", - "app.poll.optionalQuestion.label" : "Write your question (optional)...", - "app.poll.userResponse.label" : "Typed Response", - "app.poll.responseTypes.label" : "Response Types", - "app.poll.optionDelete.label" : "Delete", - "app.poll.responseChoices.label" : "Response Choices", - "app.poll.typedResponse.desc" : "Users will be presented with a text box to fill in their response.", - "app.poll.addItem.label" : "Add item", - "app.poll.start.label" : "Start Poll", - "app.poll.secretPoll.label" : "Anonymous Poll", - "app.poll.secretPoll.isSecretLabel": "The poll is anonymous – you will not be able to see individual responses.", - "app.poll.questionErr": "Providing a question is required.", - "app.poll.optionErr": "Enter a Poll option", - "app.poll.startPollDesc": "Begins the poll", - "app.poll.showRespDesc": "Displays response configuration", - "app.poll.addRespDesc": "Adds poll response input", - "app.poll.deleteRespDesc": "Removes option {0}", - "app.poll.t": "True", - "app.poll.f": "False", - "app.poll.tf": "True / False", - "app.poll.y": "Yes", - "app.poll.n": "No", - "app.poll.abstention": "Abstention", - "app.poll.yna": "Yes / No / Abstention", - "app.poll.a2": "A / B", - "app.poll.a3": "A / B / C", - "app.poll.a4": "A / B / C / D", - "app.poll.a5": "A / B / C / D / E", - "app.poll.answer.true": "True", - "app.poll.answer.false": "False", - "app.poll.answer.yes": "Yes", - "app.poll.answer.no": "No", - "app.poll.answer.abstention": "Abstention", - "app.poll.answer.a": "A", - "app.poll.answer.b": "B", - "app.poll.answer.c": "C", - "app.poll.answer.d": "D", - "app.poll.answer.e": "E", - "app.poll.liveResult.usersTitle": "Users", - "app.poll.liveResult.responsesTitle": "Response", - "app.poll.liveResult.secretLabel": "This is an anonymous poll. Individual responses are not shown.", - "app.poll.removePollOpt": "Removed Poll option {0}", - "app.poll.emptyPollOpt": "Blank", - "app.polling.pollingTitle": "Polling options", - "app.polling.pollQuestionTitle": "Polling Question", - "app.polling.submitLabel": "Submit", - "app.polling.submitAriaLabel": "Submit poll response", - "app.polling.responsePlaceholder": "Enter answer", - "app.polling.responseSecret": "Anonymous poll – the presenter can't see your answer.", - "app.polling.responseNotSecret": "Normal poll – the presenter can see your answer.", - "app.polling.pollAnswerLabel": "Poll answer {0}", - "app.polling.pollAnswerDesc": "Select this option to vote for {0}", - "app.failedMessage": "Apologies, trouble connecting to the server.", - "app.downloadPresentationButton.label": "Download the original presentation", - "app.connectingMessage": "Connecting ...", - "app.waitingMessage": "Disconnected. Trying to reconnect in {0} seconds ...", - "app.retryNow": "Retry now", - "app.muteWarning.label": "Click {0} to unmute yourself.", - "app.muteWarning.disableMessage": "Mute alerts disabled until unmute", - "app.muteWarning.tooltip": "Click to close and disable warning until next unmute", - "app.navBar.settingsDropdown.optionsLabel": "Options", - "app.navBar.settingsDropdown.fullscreenLabel": "Fullscreen Application", - "app.navBar.settingsDropdown.settingsLabel": "Settings", - "app.navBar.settingsDropdown.aboutLabel": "About", - "app.navBar.settingsDropdown.leaveSessionLabel": "Leave meeting", - "app.navBar.settingsDropdown.exitFullscreenLabel": "Exit Fullscreen", - "app.navBar.settingsDropdown.fullscreenDesc": "Make the settings menu fullscreen", - "app.navBar.settingsDropdown.settingsDesc": "Change the general settings", - "app.navBar.settingsDropdown.aboutDesc": "Show information about the client", - "app.navBar.settingsDropdown.leaveSessionDesc": "Leave the meeting", - "app.navBar.settingsDropdown.exitFullscreenDesc": "Exit fullscreen mode", - "app.navBar.settingsDropdown.hotkeysLabel": "Keyboard shortcuts", - "app.navBar.settingsDropdown.hotkeysDesc": "Listing of available keyboard shortcuts", - "app.navBar.settingsDropdown.helpLabel": "Help", - "app.navBar.settingsDropdown.openAppLabel": "Open in BigBlueButton Tablet app", - "app.navBar.settingsDropdown.helpDesc": "Links user to video tutorials (opens new tab)", - "app.navBar.settingsDropdown.endMeetingDesc": "Terminates the current meeting", - "app.navBar.settingsDropdown.endMeetingLabel": "End meeting", - "app.navBar.userListToggleBtnLabel": "User list toggle", - "app.navBar.toggleUserList.ariaLabel": "Users and messages toggle", - "app.navBar.toggleUserList.newMessages": "with new message notification", - "app.navBar.toggleUserList.newMsgAria": "New message from {0}", - "app.navBar.recording": "This session is being recorded", - "app.navBar.recording.on": "Recording", - "app.navBar.recording.off": "Not recording", - "app.navBar.emptyAudioBrdige": "No active microphone. Share your microphone to add audio to this recording.", - "app.leaveConfirmation.confirmLabel": "Leave", - "app.leaveConfirmation.confirmDesc": "Logs you out of the meeting", - "app.endMeeting.title": "End {0}", - "app.endMeeting.description": "This action will end the session for {0} active user(s). Are you sure you want to end this session?", - "app.endMeeting.noUserDescription": "Are you sure you want to end this session?", - "app.endMeeting.contentWarning": "Chat messages, shared notes, whiteboard content and shared documents for this session will no longer be directly accessible", - "app.endMeeting.yesLabel": "End session for all users", - "app.endMeeting.noLabel": "No", - "app.about.title": "About", - "app.about.version": "Client build:", - "app.about.version_label": "BigBlueButton version:", - "app.about.copyright": "Copyright:", - "app.about.confirmLabel": "OK", - "app.about.confirmDesc": "OK", - "app.about.dismissLabel": "Cancel", - "app.about.dismissDesc": "Close about client information", - "app.mobileAppModal.title": "Open BigBlueButton Tablet app", - "app.mobileAppModal.description": "Do you have the BigBlueButton Tablet app installed on your device?", - "app.mobileAppModal.openApp": "Yes, open the app now", - "app.mobileAppModal.obtainUrlMsg": "Obtaining meeting URL", - "app.mobileAppModal.obtainUrlErrorMsg": "Error trying to obtain meeting URL", - "app.mobileAppModal.openStore": "No, open the App Store to download", - "app.mobileAppModal.dismissLabel": "Cancel", - "app.mobileAppModal.dismissDesc": "Close", - "app.mobileAppModal.userConnectedWithSameId": "The user {0} just connected using the same ID as you.", - "app.actionsBar.changeStatusLabel": "Change status", - "app.actionsBar.muteLabel": "Mute", - "app.actionsBar.unmuteLabel": "Unmute", - "app.actionsBar.camOffLabel": "Camera off", - "app.actionsBar.raiseLabel": "Raise", - "app.actionsBar.label": "Actions bar", - "app.actionsBar.actionsDropdown.restorePresentationLabel": "Restore presentation", - "app.actionsBar.actionsDropdown.restorePresentationDesc": "Button to restore presentation after it has been minimized", - "app.actionsBar.actionsDropdown.minimizePresentationLabel": "Minimize presentation", - "app.actionsBar.actionsDropdown.minimizePresentationDesc": "Button used to minimize presentation", - "app.actionsBar.actionsDropdown.layoutModal": "Layout Settings Modal", - "app.actionsBar.actionsDropdown.shareCameraAsContent": "Share camera as content", - "app.actionsBar.actionsDropdown.unshareCameraAsContent": "Stop camera as content", - "app.screenshare.screenShareLabel" : "Screen share", - "app.cameraAsContent.cameraAsContentLabel" : "Present camera", - "app.submenu.application.applicationSectionTitle": "Application", - "app.submenu.application.animationsLabel": "Animations", - "app.submenu.application.audioFilterLabel": "Audio Filters for Microphone", - "app.submenu.application.wbToolbarsAutoHideLabel": "Auto Hide Whiteboard Toolbars", - "app.submenu.application.darkThemeLabel": "Dark mode", - "app.submenu.application.fontSizeControlLabel": "Font size", - "app.submenu.application.increaseFontBtnLabel": "Increase application font size", - "app.submenu.application.decreaseFontBtnLabel": "Decrease application font size", - "app.submenu.application.currentSize": "currently {0}", - "app.submenu.application.languageLabel": "Application Language", - "app.submenu.application.languageOptionLabel": "Choose language", - "app.submenu.application.noLocaleOptionLabel": "No active locales", - "app.submenu.application.paginationEnabledLabel": "Video pagination", - "app.submenu.application.layoutOptionLabel": "Layout type", - "app.submenu.application.pushLayoutLabel": "Push layout", - "app.submenu.application.localeDropdown.af": "Afrikaans", - "app.submenu.application.localeDropdown.ar": "Arabic", - "app.submenu.application.localeDropdown.az": "Azerbaijani", - "app.submenu.application.localeDropdown.bg-BG": "Bulgarian", - "app.submenu.application.localeDropdown.bn": "Bengali", - "app.submenu.application.localeDropdown.ca": "Catalan", - "app.submenu.application.localeDropdown.cs-CZ": "Czech", - "app.submenu.application.localeDropdown.da": "Danish", - "app.submenu.application.localeDropdown.de": "German", - "app.submenu.application.localeDropdown.dv": "Dhivehi", - "app.submenu.application.localeDropdown.el-GR": "Greek (Greece)", - "app.submenu.application.localeDropdown.en": "English", - "app.submenu.application.localeDropdown.eo": "Esperanto", - "app.submenu.application.localeDropdown.es": "Spanish", - "app.submenu.application.localeDropdown.es-419": "Spanish (Latin America)", - "app.submenu.application.localeDropdown.es-ES": "Spanish (Spain)", - "app.submenu.application.localeDropdown.es-MX": "Spanish (Mexico)", - "app.submenu.application.localeDropdown.et": "Estonian", - "app.submenu.application.localeDropdown.eu": "Basque", - "app.submenu.application.localeDropdown.fa-IR": "Persian", - "app.submenu.application.localeDropdown.fi": "Finnish", - "app.submenu.application.localeDropdown.fr": "French", - "app.submenu.application.localeDropdown.gl": "Galician", - "app.submenu.application.localeDropdown.he": "Hebrew", - "app.submenu.application.localeDropdown.hi-IN": "Hindi", - "app.submenu.application.localeDropdown.hr": "Croatian", - "app.submenu.application.localeDropdown.hu-HU": "Hungarian", - "app.submenu.application.localeDropdown.hy": "Armenian", - "app.submenu.application.localeDropdown.id": "Indonesian", - "app.submenu.application.localeDropdown.it-IT": "Italian", - "app.submenu.application.localeDropdown.ja": "Japanese", - "app.submenu.application.localeDropdown.ka": "Georgian", - "app.submenu.application.localeDropdown.km": "Khmer", - "app.submenu.application.localeDropdown.kn": "Kannada", - "app.submenu.application.localeDropdown.ko-KR": "Korean (Korea)", - "app.submenu.application.localeDropdown.lo-LA": "Lao", - "app.submenu.application.localeDropdown.lt-LT": "Lithuanian", - "app.submenu.application.localeDropdown.lv": "Latvian", - "app.submenu.application.localeDropdown.ml": "Malayalam", - "app.submenu.application.localeDropdown.mn-MN": "Mongolian", - "app.submenu.application.localeDropdown.nb-NO": "Norwegian (bokmal)", - "app.submenu.application.localeDropdown.nl": "Dutch", - "app.submenu.application.localeDropdown.oc": "Occitan", - "app.submenu.application.localeDropdown.pl-PL": "Polish", - "app.submenu.application.localeDropdown.pt": "Portuguese", - "app.submenu.application.localeDropdown.pt-BR": "Portuguese (Brazil)", - "app.submenu.application.localeDropdown.ro-RO": "Romanian", - "app.submenu.application.localeDropdown.ru": "Russian", - "app.submenu.application.localeDropdown.sk-SK": "Slovak (Slovakia)", - "app.submenu.application.localeDropdown.sl": "Slovenian", - "app.submenu.application.localeDropdown.sr": "Serbian", - "app.submenu.application.localeDropdown.sv-SE": "Swedish", - "app.submenu.application.localeDropdown.ta": "Tamil", - "app.submenu.application.localeDropdown.te": "Telugu", - "app.submenu.application.localeDropdown.th": "Thai", - "app.submenu.application.localeDropdown.tr": "Turkish", - "app.submenu.application.localeDropdown.tr-TR": "Turkish (Turkey)", - "app.submenu.application.localeDropdown.uk-UA": "Ukrainian", - "app.submenu.application.localeDropdown.vi": "Vietnamese", - "app.submenu.application.localeDropdown.vi-VN": "Vietnamese (Vietnam)", - "app.submenu.application.localeDropdown.zh-CN": "Chinese Simplified (China)", - "app.submenu.application.localeDropdown.zh-TW": "Chinese Traditional (Taiwan)", - "app.submenu.notification.SectionTitle": "Notifications", - "app.submenu.notification.Desc": "Define how and what you will be notified.", - "app.submenu.notification.audioAlertLabel": "Audio Alerts", - "app.submenu.notification.pushAlertLabel": "Popup Alerts", - "app.submenu.notification.messagesLabel": "Chat Message", - "app.submenu.notification.userJoinLabel": "User Join", - "app.submenu.notification.userLeaveLabel": "User Leave", - "app.submenu.notification.guestWaitingLabel": "Guest Waiting Approval", - "app.submenu.audio.micSourceLabel": "Microphone source", - "app.submenu.audio.speakerSourceLabel": "Speaker source", - "app.submenu.audio.streamVolumeLabel": "Your audio stream volume", - "app.submenu.video.title": "Video", - "app.submenu.video.videoSourceLabel": "View source", - "app.submenu.video.videoOptionLabel": "Choose view source", - "app.submenu.video.videoQualityLabel": "Video quality", - "app.submenu.video.qualityOptionLabel": "Choose the video quality", - "app.submenu.video.participantsCamLabel": "Viewing participants webcams", - "app.settings.applicationTab.label": "Application", - "app.settings.audioTab.label": "Audio", - "app.settings.videoTab.label": "Video", - "app.settings.usersTab.label": "Participants", - "app.settings.main.label": "Settings", - "app.settings.main.cancel.label": "Cancel", - "app.settings.main.cancel.label.description": "Discards the changes and closes the settings menu", - "app.settings.main.save.label": "Save", - "app.settings.main.save.label.description": "Saves the changes and closes the settings menu", - "app.settings.dataSavingTab.label": "Data savings", - "app.settings.dataSavingTab.webcam": "Enable other participants webcams", - "app.settings.dataSavingTab.screenShare": "Enable other participants desktop sharing", - "app.settings.dataSavingTab.description": "To save your bandwidth adjust what's currently being displayed.", - "app.settings.save-notification.label": "Settings have been saved", - "app.statusNotifier.lowerHands": "Lower Hands", - "app.statusNotifier.lowerHandDescOneUser": "Lower {0}'s hand", - "app.statusNotifier.raisedHandsTitle": "Raised Hands", - "app.statusNotifier.raisedHandDesc": "{0} raised their hands", - "app.statusNotifier.raisedHandDescOneUser": "{0} raised hand", - "app.statusNotifier.and": "and", - "app.switch.onLabel": "ON", - "app.switch.offLabel": "OFF", - "app.talkingIndicator.ariaMuteDesc" : "Select to mute user", - "app.talkingIndicator.isTalking" : "{0} is talking", - "app.talkingIndicator.moreThanMaxIndicatorsTalking" : "{0}+ are talking", - "app.talkingIndicator.moreThanMaxIndicatorsWereTalking" : "{0}+ were talking", - "app.talkingIndicator.wasTalking" : "{0} stopped talking", - "app.actionsBar.actionsDropdown.actionsLabel": "Actions", - "app.actionsBar.actionsDropdown.activateTimerLabel": "Activate stopwatch", - "app.actionsBar.actionsDropdown.deactivateTimerLabel": "Deactivate stopwatch", - "app.actionsBar.actionsDropdown.presentationLabel": "Upload/Manage presentations", - "app.actionsBar.actionsDropdown.initPollLabel": "Initiate a poll", - "app.actionsBar.actionsDropdown.desktopShareLabel": "Share your screen", - "app.actionsBar.actionsDropdown.stopDesktopShareLabel": "Stop sharing your screen", - "app.actionsBar.actionsDropdown.presentationDesc": "Upload your presentation", - "app.actionsBar.actionsDropdown.initPollDesc": "Initiate a poll", - "app.actionsBar.actionsDropdown.desktopShareDesc": "Share your screen with others", - "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Stop sharing your screen with", - "app.actionsBar.actionsDropdown.pollBtnLabel": "Start a poll", - "app.actionsBar.actionsDropdown.pollBtnDesc": "Toggles poll pane", - "app.actionsBar.actionsDropdown.saveUserNames": "Save user names", - "app.actionsBar.actionsDropdown.createBreakoutRoom": "Create breakout rooms", - "app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "create breakouts for split the current meeting ", - "app.actionsBar.actionsDropdown.captionsLabel": "Write closed captions", - "app.actionsBar.actionsDropdown.captionsDesc": "Toggles captions pane", - "app.actionsBar.actionsDropdown.takePresenter": "Take presenter", - "app.actionsBar.actionsDropdown.takePresenterDesc": "Assign yourself as the new presenter", - "app.actionsBar.actionsDropdown.selectRandUserLabel": "Select random user", - "app.actionsBar.actionsDropdown.selectRandUserDesc": "Chooses a user from available viewers at random", - "app.actionsBar.actionsDropdown.propagateLayoutLabel": "Propagate layout", - "app.actionsBar.interactions.interactions": "Interactions", - "app.actionsBar.interactions.raiseHand": "Raise your hand", - "app.actionsBar.interactions.lowHand": "Lower your hand", - "app.actionsBar.interactions.addReaction": "Add a reaction", - "app.actionsBar.emojiMenu.statusTriggerLabel": "Set status", - "app.actionsBar.emojiMenu.awayLabel": "Away", - "app.actionsBar.emojiMenu.awayDesc": "Change your status to away", - "app.actionsBar.emojiMenu.raiseHandLabel": "Raise hand", - "app.actionsBar.emojiMenu.lowerHandLabel": "Lower hand", - "app.actionsBar.emojiMenu.raiseHandDesc": "Raise your hand to ask a question", - "app.actionsBar.emojiMenu.neutralLabel": "Undecided", - "app.actionsBar.emojiMenu.neutralDesc": "Change your status to undecided", - "app.actionsBar.emojiMenu.confusedLabel": "Confused", - "app.actionsBar.emojiMenu.confusedDesc": "Change your status to confused", - "app.actionsBar.emojiMenu.sadLabel": "Sad", - "app.actionsBar.emojiMenu.sadDesc": "Change your status to sad", - "app.actionsBar.emojiMenu.happyLabel": "Happy", - "app.actionsBar.emojiMenu.happyDesc": "Change your status to happy", - "app.actionsBar.emojiMenu.noneLabel": "Clear Status", - "app.actionsBar.emojiMenu.noneDesc": "Clear your status", - "app.actionsBar.emojiMenu.applauseLabel": "Applaud", - "app.actionsBar.emojiMenu.applauseDesc": "Change your status to applause", - "app.actionsBar.emojiMenu.thumbsUpLabel": "Thumbs up", - "app.actionsBar.emojiMenu.thumbsUpDesc": "Change your status to thumbs up", - "app.actionsBar.emojiMenu.thumbsDownLabel": "Thumbs down", - "app.actionsBar.emojiMenu.thumbsDownDesc": "Change your status to thumbs down", - "app.actionsBar.currentStatusDesc": "current status {0}", - "app.actionsBar.captions.start": "Start viewing closed captions", - "app.actionsBar.captions.stop": "Stop viewing closed captions", - "app.audioNotification.audioFailedError1001": "WebSocket disconnected (error 1001)", - "app.audioNotification.audioFailedError1002": "Could not make a WebSocket connection (error 1002)", - "app.audioNotification.audioFailedError1003": "Browser version not supported (error 1003)", - "app.audioNotification.audioFailedError1004": "Failure on call (reason={0}) (error 1004)", - "app.audioNotification.audioFailedError1005": "Call ended unexpectedly (error 1005)", - "app.audioNotification.audioFailedError1006": "Call timed out (error 1006)", - "app.audioNotification.audioFailedError1007": "Connection failure (ICE error 1007)", - "app.audioNotification.audioFailedError1008": "Transfer failed (error 1008)", - "app.audioNotification.audioFailedError1009": "Could not fetch STUN/TURN server information (error 1009)", - "app.audioNotification.audioFailedError1010": "Connection negotiation timeout (ICE error 1010)", - "app.audioNotification.audioFailedError1011": "Connection timeout (ICE error 1011)", - "app.audioNotification.audioFailedError1012": "Connection closed (ICE error 1012)", - "app.audioNotification.audioFailedMessage": "Your audio connection failed to connect", - "app.audioNotification.mediaFailedMessage": "getUserMicMedia failed as only secure origins are allowed", - "app.audioNotification.deviceChangeFailed": "Audio device change failed. Check if the chosen device is properly set up and available", - "app.audioNotification.closeLabel": "Close", - "app.audioNotificaion.reconnectingAsListenOnly": "Microphone has been locked for viewers, you are being connected as listen only", - "app.breakoutJoinConfirmation.title": "Join breakout room", - "app.breakoutJoinConfirmation.message": "Do you want to join", - "app.breakoutJoinConfirmation.confirmDesc": "Join you to the breakout room", - "app.breakoutJoinConfirmation.dismissLabel": "Cancel", - "app.breakoutJoinConfirmation.dismissDesc": "Closes and rejects joining the breakout room", - "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": "Change duration", - "app.breakout.dropdown.destroyAll": "End breakout rooms", - "app.breakout.dropdown.options": "Breakout Options", - "app.breakout.dropdown.manageUsers": "Manage users", - "app.calculatingBreakoutTimeRemaining": "Calculating remaining time ...", - "app.audioModal.ariaTitle": "Join audio modal", - "app.audioModal.microphoneLabel": "Microphone", - "app.audioModal.listenOnlyLabel": "Listen only", - "app.audioModal.microphoneDesc": "Joins audio conference with mic", - "app.audioModal.listenOnlyDesc": "Joins audio conference as listen only", - "app.audioModal.audioChoiceLabel": "How would you like to join the audio?", - "app.audioModal.iOSBrowser": "Audio/Video not supported", - "app.audioModal.iOSErrorDescription": "At this time audio and video are not supported on Chrome for iOS.", - "app.audioModal.iOSErrorRecommendation": "We recommend using Safari iOS.", - "app.audioModal.audioChoiceDesc": "Select how to join the audio in this meeting", - "app.audioModal.unsupportedBrowserLabel": "It looks like you're using a browser that is not fully supported. Please use either {0} or {1} for full support.", - "app.audioModal.closeLabel": "Close", - "app.audioModal.yes": "Yes", - "app.audioModal.no": "No", - "app.audioModal.yes.arialabel" : "Echo is audible", - "app.audioModal.no.arialabel" : "Echo is inaudible", - "app.audioModal.echoTestTitle": "This is a private echo test. Speak a few words. Did you hear audio?", - "app.audioModal.settingsTitle": "Change your audio settings", - "app.audioModal.helpTitle": "There was an issue with your media devices", - "app.audioModal.helpText": "Did you give permission for access to your microphone? Note that a dialog should appear when you try to join audio, asking for your media device permissions, please accept that in order to join the audio conference. If that is not the case, try changing your microphone permissions in your browser's settings.", - "app.audioModal.help.noSSL": "This page is unsecured. For microphone access to be allowed the page must be served over HTTPS. Please contact the server administrator.", - "app.audioModal.help.macNotAllowed": "It looks like your Mac System Preferences are blocking access to your microphone. Open System Preferences > Security & Privacy > Privacy > Microphone, and verify that the browser you're using is checked.", - "app.audioModal.audioDialTitle": "Join using your phone", - "app.audioDial.audioDialDescription": "Dial", - "app.audioDial.audioDialConfrenceText": "and enter the conference PIN number:", - "app.audioModal.autoplayBlockedDesc": "We need your permission to play audio.", - "app.audioModal.playAudio": "Play audio", - "app.audioModal.playAudio.arialabel" : "Play audio", - "app.audioDial.tipIndicator": "Tip", - "app.audioDial.tipMessage": "Press the '0' key on your phone to mute/unmute yourself.", - "app.audioModal.connecting": "Establishing audio connection", - "app.audioManager.joinedAudio": "You have joined the audio conference", - "app.audioManager.joinedEcho": "You have joined the echo test", - "app.audioManager.leftAudio": "You have left the audio conference", - "app.audioManager.reconnectingAudio": "Attempting to reconnect audio", - "app.audioManager.genericError": "Error: An error has occurred, please try again", - "app.audioManager.connectionError": "Error: Connection error", - "app.audioManager.requestTimeout": "Error: There was a timeout in the request", - "app.audioManager.invalidTarget": "Error: Tried to request something to an invalid target", - "app.audioManager.mediaError": "Error: There was an issue getting your media devices", - "app.audio.joinAudio": "Join audio", - "app.audio.leaveAudio": "Leave audio", - "app.audio.changeAudioDevice": "Change audio device", - "app.audio.enterSessionLabel": "Enter session", - "app.audio.playSoundLabel": "Play sound", - "app.audio.stopAudioFeedback": "Stop audio feedback", - "app.audio.backLabel": "Back", - "app.audio.loading": "Loading", - "app.audio.microphones": "Microphones", - "app.audio.speakers": "Speakers", - "app.audio.noDeviceFound": "No device found", - "app.audio.audioSettings.titleLabel": "Choose your audio settings", - "app.audio.audioSettings.descriptionLabel": "Please note, a dialog will appear in your browser, requiring you to accept sharing your microphone.", - "app.audio.audioSettings.microphoneSourceLabel": "Microphone source", - "app.audio.audioSettings.speakerSourceLabel": "Speaker source", - "app.audio.audioSettings.testSpeakerLabel": "Test your speaker", - "app.audio.audioSettings.microphoneStreamLabel": "Your audio stream volume", - "app.audio.audioSettings.retryLabel": "Retry", - "app.audio.audioSettings.fallbackInputLabel": "Audio input {0}", - "app.audio.audioSettings.fallbackOutputLabel": "Audio output {0}", - "app.audio.audioSettings.defaultOutputDeviceLabel": "Default", - "app.audio.audioSettings.findingDevicesLabel": "Finding devices...", - "app.audio.listenOnly.backLabel": "Back", - "app.audio.listenOnly.closeLabel": "Close", - "app.audio.permissionsOverlay.title": "Allow access to your microphone", - "app.audio.permissionsOverlay.hint": "We need you to allow us to use your media devices in order to join you to the voice conference :)", - "app.audio.captions.button.start": "Start closed captions", - "app.audio.captions.button.stop": "Stop closed captions", - "app.audio.captions.button.language": "Language", - "app.audio.captions.button.transcription": "Transcription", - "app.audio.captions.button.transcriptionSettings": "Transcription settings", - "app.audio.captions.speech.title": "Automatic transcription", - "app.audio.captions.speech.disabled": "Disabled", - "app.audio.captions.speech.unsupported": "Your browser doesn't support speech recognition. Your audio won't be transcribed", - "app.audio.captions.select.de-DE": "German", - "app.audio.captions.select.en-US": "English", - "app.audio.captions.select.es-ES": "Spanish", - "app.audio.captions.select.fr-FR": "French", - "app.audio.captions.select.hi-ID": "Hindi", - "app.audio.captions.select.it-IT": "Italian", - "app.audio.captions.select.ja-JP": "Japanese", - "app.audio.captions.select.pt-BR": "Portuguese", - "app.audio.captions.select.ru-RU": "Russian", - "app.audio.captions.select.zh-CN": "Chinese", - "app.error.removed": "You have been removed from the conference", - "app.error.meeting.ended": "You have logged out of the conference", - "app.meeting.logout.duplicateUserEjectReason": "Duplicate user trying to join meeting", - "app.meeting.logout.permissionEjectReason": "Ejected due to permission violation", - "app.meeting.logout.ejectedFromMeeting": "You have been removed from the meeting", - "app.meeting.logout.validateTokenFailedEjectReason": "Failed to validate authorization token", - "app.meeting.logout.userInactivityEjectReason": "User inactive for too long", - "app.meeting.logout.maxParticipantsReached": "The maximum number of participants allowed for this meeting has been reached", - "app.meeting-ended.rating.legendLabel": "Feedback rating", - "app.meeting-ended.rating.starLabel": "Star", - "app.modal.close": "Close", - "app.modal.close.description": "Disregards changes and closes the modal", - "app.modal.confirm": "Done", - "app.modal.newTab": "(opens new tab)", - "app.modal.confirm.description": "Saves changes and closes the modal", - "app.modal.randomUser.noViewers.description": "No viewers available to randomly select from", - "app.modal.randomUser.selected.description": "You have been randomly selected", - "app.modal.randomUser.title": "Randomly selected user", - "app.modal.randomUser.who": "Who will be selected..?", - "app.modal.randomUser.alone": "There is only one viewer", - "app.modal.randomUser.reselect.label": "Select again", - "app.modal.randomUser.ariaLabel.title": "Randomly selected User Modal", - "app.dropdown.close": "Close", - "app.dropdown.list.item.activeLabel": "Active", - "app.error.400": "Bad Request", - "app.error.401": "Unauthorized", - "app.error.403": "You have been removed from the meeting", - "app.error.404": "Not found", - "app.error.408": "Authentication failed", - "app.error.409": "Conflict", - "app.error.410": "Meeting has ended", - "app.error.500": "Ops, something went wrong", - "app.error.503": "You have been disconnected", - "app.error.disconnected.rejoin": "You are able to refresh the page to rejoin.", - "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", - "app.error.fallback.presentation.description": "It has been logged. Please try reloading the page.", - "app.error.fallback.presentation.reloadButton": "Reload", - "app.guest.errorSeeConsole": "Error: more details in the console.", - "app.guest.noModeratorResponse": "No response from Moderator.", - "app.guest.noSessionToken": "No session Token received.", - "app.guest.windowTitle": "BigBlueButton - Guest Lobby", - "app.guest.missingToken": "Guest missing session token.", - "app.guest.missingSession": "Guest missing session.", - "app.guest.missingMeeting": "Meeting does not exist.", - "app.guest.meetingEnded": "Meeting ended.", - "app.guest.guestWait": "Please wait for a moderator to approve you joining the meeting.", - "app.guest.guestDeny": "Guest denied of joining the meeting.", - "app.guest.seatWait": "Guest waiting for a seat in the meeting.", - "app.guest.allow": "Guest approved and redirecting to meeting.", - "app.guest.firstPositionInWaitingQueue": "You are the first in line!", - "app.guest.positionInWaitingQueue": "Your current position in waiting queue: ", - "app.guest.guestInvalid": "Guest user is invalid", - "app.guest.meetingForciblyEnded": "You cannot join a meeting that has already been forcibly ended", - "app.userList.guest.waitingUsers": "Waiting Users", - "app.userList.guest.waitingUsersTitle": "User Management", - "app.userList.guest.optionTitle": "Review Pending Users", - "app.userList.guest.allowAllAuthenticated": "Allow all authenticated", - "app.userList.guest.allowAllGuests": "Allow all guests", - "app.userList.guest.allowEveryone": "Allow everyone", - "app.userList.guest.denyEveryone": "Deny everyone", - "app.userList.guest.pendingUsers": "{0} Pending Users", - "app.userList.guest.noPendingUsers": "Currently no pending users...", - "app.userList.guest.pendingGuestUsers": "{0} Pending Guest Users", - "app.userList.guest.pendingGuestAlert": "Has joined the session and is waiting for your approval.", - "app.userList.guest.rememberChoice": "Remember choice", - "app.userList.guest.emptyMessage": "There is currently no message", - "app.userList.guest.inputPlaceholder": "Message to the guests' lobby", - "app.userList.guest.privateInputPlaceholder": "Message to {0}", - "app.userList.guest.privateMessageLabel": "Message", - "app.userList.guest.acceptLabel": "Accept", - "app.userList.guest.denyLabel": "Deny", - "app.userList.guest.feedbackMessage": "Action applied: ", - "app.user-info.title": "Directory Lookup", - "app.toast.breakoutRoomEnded": "The breakout room ended. Please rejoin in the audio.", - "app.toast.chat.public": "New Public Chat message", - "app.toast.chat.private": "New Private Chat message", - "app.toast.chat.system": "System", - "app.toast.chat.poll": "Poll Results", - "app.toast.chat.pollClick": "Poll results were published. Click here to see.", - "app.toast.clearedEmoji.label": "Emoji status cleared", - "app.toast.setEmoji.label": "Emoji status set to {0}", - "app.toast.meetingMuteOn.label": "All users have been muted", - "app.toast.meetingMuteOnViewers.label": "All viewers have been muted", - "app.toast.meetingMuteOff.label": "Meeting mute turned off", - "app.toast.setEmoji.raiseHand": "You have raised your hand", - "app.toast.setEmoji.lowerHand": "Your hand has been lowered", - "app.toast.setEmoji.away": "You have set your status to away", - "app.toast.setEmoji.notAway": "You removed your away status", - "app.toast.promotedLabel": "You have been promoted to Moderator", - "app.toast.demotedLabel": "You have been demoted to Viewer", - "app.notification.recordingStart": "This session is now being recorded", - "app.notification.recordingStop": "This session is not being recorded", - "app.notification.recordingPaused": "This session is not being recorded anymore", - "app.notification.recordingAriaLabel": "Recorded time ", - "app.notification.userJoinPushAlert": "{0} joined the session", - "app.notification.userLeavePushAlert": "{0} left the session", - "app.submenu.notification.raiseHandLabel": "Raise hand", - "app.shortcut-help.title": "Keyboard shortcuts", - "app.shortcut-help.accessKeyNotAvailable": "Access keys not available", - "app.shortcut-help.comboLabel": "Combo", - "app.shortcut-help.alternativeLabel": "Alternative", - "app.shortcut-help.functionLabel": "Function", - "app.shortcut-help.closeLabel": "Close", - "app.shortcut-help.closeDesc": "Closes keyboard shortcuts modal", - "app.shortcut-help.openOptions": "Open Options", - "app.shortcut-help.toggleUserList": "Toggle UserList", - "app.shortcut-help.toggleMute": "Mute / Unmute", - "app.shortcut-help.togglePublicChat": "Toggle Public Chat (User list must be open)", - "app.shortcut-help.hidePrivateChat": "Hide private chat", - "app.shortcut-help.closePrivateChat": "Close private chat", - "app.shortcut-help.openActions": "Open actions menu", - "app.shortcut-help.raiseHand": "Toggle Raise Hand", - "app.shortcut-help.openDebugWindow": "Open debug window", - "app.shortcut-help.openStatus": "Open status menu", - "app.shortcut-help.togglePan": "Activate Pan tool (Presenter)", - "app.shortcut-help.toggleFullscreen": "Toggle Full-screen (Presenter)", - "app.shortcut-help.nextSlideDesc": "Next slide (Presenter)", - "app.shortcut-help.previousSlideDesc": "Previous slide (Presenter)", - "app.shortcut-help.togglePanKey": "Spacebar", - "app.shortcut-help.toggleFullscreenKey": "Enter", - "app.shortcut-help.nextSlideKey": "Right Arrow", - "app.shortcut-help.previousSlideKey": "Left Arrow", - "app.shortcut-help.select": "Select Tool", - "app.shortcut-help.pencil": "Pencil", - "app.shortcut-help.eraser": "Eraser", - "app.shortcut-help.rectangle": "Rectangle", - "app.shortcut-help.elipse": "Elipse", - "app.shortcut-help.triangle": "Triangle", - "app.shortcut-help.line": "Line", - "app.shortcut-help.arrow": "Arrow", - "app.shortcut-help.text": "Text Tool", - "app.shortcut-help.note": "Sticky Note", - "app.shortcut-help.general": "General", - "app.shortcut-help.presentation": "Presentation", - "app.shortcut-help.whiteboard": "Whiteboard", - "app.shortcut-help.zoomIn": "Zoom In", - "app.shortcut-help.zoomOut": "Zoom Out", - "app.shortcut-help.zoomFit": "Reset Zoom", - "app.shortcut-help.zoomSelect": "Zoom to Selection", - "app.shortcut-help.flipH": "Flip Horizontal", - "app.shortcut-help.flipV": "Flip Vertical", - "app.shortcut-help.lock": "Lock / Unlock", - "app.shortcut-help.moveToFront": "Move to Front", - "app.shortcut-help.moveToBack": "Move to Back", - "app.shortcut-help.moveForward": "Move Forward", - "app.shortcut-help.moveBackward": "Move Backward", - "app.shortcut-help.undo": "Undo", - "app.shortcut-help.redo": "Redo", - "app.shortcut-help.cut": "Cut", - "app.shortcut-help.copy": "Copy", - "app.shortcut-help.paste": "Paste", - "app.shortcut-help.selectAll": "Select All", - "app.shortcut-help.delete": "Delete", - "app.shortcut-help.duplicate": "Duplicate", - "app.lock-viewers.title": "Lock viewers", - "app.lock-viewers.description": "These options enable you to restrict viewers from using specific features.", - "app.lock-viewers.featuresLable": "Feature", - "app.lock-viewers.lockStatusLabel": "Status", - "app.lock-viewers.webcamLabel": "Share webcam", - "app.lock-viewers.otherViewersWebcamLabel": "See other viewers webcams", - "app.lock-viewers.microphoneLable": "Share microphone", - "app.lock-viewers.PublicChatLabel": "Send Public chat messages", - "app.lock-viewers.PrivateChatLable": "Send Private chat messages", - "app.lock-viewers.notesLabel": "Edit Shared Notes", - "app.lock-viewers.userListLabel": "See other viewers in the Users list", - "app.lock-viewers.ariaTitle": "Lock viewers settings modal", - "app.lock-viewers.button.apply": "Apply", - "app.lock-viewers.button.cancel": "Cancel", - "app.lock-viewers.locked": "Locked", - "app.lock-viewers.hideViewersCursor": "See other viewers cursors", - "app.lock-viewers.hideAnnotationsLabel": "See other viewers annotations", - "app.guest-policy.ariaTitle": "Guest policy settings modal", - "app.guest-policy.title": "Guest policy", - "app.guest-policy.description": "Change meeting guest policy setting", - "app.guest-policy.button.askModerator": "Ask moderator", - "app.guest-policy.button.alwaysAccept": "Always accept", - "app.guest-policy.button.alwaysDeny": "Always deny", - "app.guest-policy.policyBtnDesc": "Sets meeting guest policy", - "app.connection-status.ariaTitle": "Connection status modal", - "app.connection-status.title": "Connection status", - "app.connection-status.description": "View users' connection status", - "app.connection-status.empty": "There are currently no reported connection issues", - "app.connection-status.more": "more", - "app.connection-status.copy": "Copy Stats", - "app.connection-status.copied": "Copied!", - "app.connection-status.jitter": "Jitter", - "app.connection-status.label": "Connection status", - "app.connection-status.settings": "Adjusting Your Settings", - "app.connection-status.no": "No", - "app.connection-status.notification": "Loss in your connection was detected", - "app.connection-status.offline": "offline", - "app.connection-status.clientNotRespondingWarning": "Client not responding", - "app.connection-status.audioUploadRate": "Audio Upload Rate", - "app.connection-status.audioDownloadRate": "Audio Download Rate", - "app.connection-status.videoUploadRate": "Video Upload Rate", - "app.connection-status.videoDownloadRate": "Video Download Rate", - "app.connection-status.lostPackets": "Lost packets", - "app.connection-status.usingTurn": "Using TURN", - "app.connection-status.yes": "Yes", - "app.connection-status.connectionStats": "Connection Stats", - "app.connection-status.myLogs": "My Logs", - "app.connection-status.sessionLogs": "Session Logs", - "app.connection-status.next": "Next page", - "app.connection-status.prev": "Previous page", - "app.learning-dashboard.label": "Learning Analytics Dashboard", - "app.learning-dashboard.description": "Dashboard with users activities", - "app.learning-dashboard.clickHereToOpen": "Open Learning Analytics Dashboard", - "app.recording.startTitle": "Start recording", - "app.recording.stopTitle": "Pause recording", - "app.recording.resumeTitle": "Resume recording", - "app.recording.startDescription": "You can select the record button again later to pause the recording.", - "app.recording.stopDescription": "Are you sure you want to pause the recording? You can resume by selecting the record button again.", - "app.recording.notify.title": "Recording has started", - "app.recording.notify.description": "A recording will be available based on the remainder of this session", - "app.recording.notify.continue": "Continue", - "app.recording.notify.leave": "Leave session", - "app.recording.notify.continueLabel" : "Accept recording and continue", - "app.recording.notify.leaveLabel" : "Do not accept recording and leave meeting", - "app.videoPreview.cameraLabel": "Camera", - "app.videoPreview.profileLabel": "Quality", - "app.videoPreview.quality.low": "Low", - "app.videoPreview.quality.medium": "Medium", - "app.videoPreview.quality.high": "High", - "app.videoPreview.quality.hd": "High definition", - "app.videoPreview.cancelLabel": "Cancel", - "app.videoPreview.closeLabel": "Close", - "app.videoPreview.findingWebcamsLabel": "Finding webcams", - "app.videoPreview.startSharingLabel": "Start sharing", - "app.videoPreview.stopSharingLabel": "Stop sharing", - "app.videoPreview.stopSharingAllLabel": "Stop all", - "app.videoPreview.sharedCameraLabel": "This camera is already being shared", - "app.videoPreview.webcamOptionLabel": "Choose webcam", - "app.videoPreview.webcamPreviewLabel": "Webcam preview", - "app.videoPreview.webcamSettingsTitle": "Webcam settings", - "app.videoPreview.webcamEffectsTitle": "Webcam visual effects", - "app.videoPreview.cameraAsContentSettingsTitle": "Present Camera", - "app.videoPreview.webcamVirtualBackgroundLabel": "Virtual background settings", - "app.videoPreview.webcamVirtualBackgroundDisabledLabel": "This device does not support virtual backgrounds", - "app.videoPreview.webcamNotFoundLabel": "Webcam not found", - "app.videoPreview.profileNotFoundLabel": "No supported camera profile", - "app.videoPreview.brightness": "Brightness", - "app.videoPreview.wholeImageBrightnessLabel": "Whole image", - "app.videoPreview.wholeImageBrightnessDesc": "Applies brightness to stream and background image", - "app.videoPreview.sliderDesc": "Increase or decrease levels of brightness", - "app.video.joinVideo": "Share webcam", - "app.video.connecting": "Webcam sharing is starting ...", - "app.video.leaveVideo": "Stop sharing webcam", - "app.video.videoSettings": "Video settings", - "app.video.visualEffects": "Visual effects", - "app.video.advancedVideo": "Open advanced settings", - "app.video.iceCandidateError": "Error on adding ICE candidate", - "app.video.iceConnectionStateError": "Connection failure (ICE error 1107)", - "app.video.permissionError": "Error on sharing webcam. Please check permissions", - "app.video.sharingError": "Error on sharing webcam", - "app.video.abortError": "An unknown problem occurred which prevented your camera from being used", - "app.video.overconstrainedError": "Your camera does not support this quality profile", - "app.video.securityError": "Your browser has disabled camera usage. Try a different browser", - "app.video.typeError": "Invalid camera quality profile. Contact your administrator", - "app.video.notFoundError": "No webcams found. Please make sure there's one connected", - "app.video.notAllowed": "Permission to access webcams needs to be granted", - "app.video.notSupportedError": "Browser is not supported. Try again using a different browser or device", - "app.video.notReadableError": "Could not get webcam video. Please make sure another program is not using the webcam ", - "app.video.timeoutError": "Browser did not respond in time.", - "app.video.genericError": "An unknown error has occurred with the device ({0})", - "app.video.inactiveError": "Your webcam stopped unexpectedly. Please review your browser's permissions", - "app.video.mediaTimedOutError": "Your webcam stream has been interrupted. Try sharing it again", - "app.video.mediaFlowTimeout1020": "Media could not reach the server (error 1020)", - "app.video.suggestWebcamLock": "Enforce lock setting to viewers webcams?", - "app.video.suggestWebcamLockReason": "(this will improve the stability of the meeting)", - "app.video.enable": "Enable", - "app.video.cancel": "Cancel", - "app.video.swapCam": "Swap", - "app.video.swapCamDesc": "swap the direction of webcams", - "app.video.videoLocked": "Webcam sharing locked", - "app.video.videoButtonDesc": "Share webcam", - "app.video.videoMenu": "Video menu", - "app.video.videoMenuDisabled": "Video menu Webcam is disabled in settings", - "app.video.videoMenuDesc": "Open video menu dropdown", - "app.video.pagination.prevPage": "See previous videos", - "app.video.pagination.nextPage": "See next videos", - "app.video.clientDisconnected": "Webcam cannot be shared due to connection issues", - "app.video.virtualBackground.none": "None", - "app.video.virtualBackground.blur": "Blur", - "app.video.virtualBackground.home": "Home", - "app.video.virtualBackground.board": "Board", - "app.video.virtualBackground.coffeeshop": "Coffeeshop", - "app.video.virtualBackground.background": "Background", - "app.video.virtualBackground.backgroundWithIndex": "Background {0}", - "app.video.virtualBackground.custom": "Upload from your computer", - "app.video.virtualBackground.remove": "Remove added image", - "app.video.virtualBackground.genericError": "Failed to apply camera effect. Try again.", - "app.video.virtualBackground.camBgAriaDesc": "Sets webcam virtual background to {0}", - "app.video.virtualBackground.maximumFileSizeExceeded": "Maximum file size exceeded. ({0}MB)", - "app.video.virtualBackground.typeNotAllowed": "File type not allowed.", - "app.video.virtualBackground.errorOnRead": "Something went wrong when reading the file.", - "app.video.virtualBackground.uploaded": "Uploaded", - "app.video.virtualBackground.uploading": "Uploading...", - "app.video.virtualBackground.button.customDesc": "Adds a new virtual background image", - "app.video.camCapReached": "You cannot share more cameras", - "app.video.meetingCamCapReached": "Meeting reached it's simultaneous cameras limit", - "app.video.dropZoneLabel": "Drop here", - "app.fullscreenButton.label": "Make {0} fullscreen", - "app.fullscreenUndoButton.label": "Undo {0} fullscreen", - "app.switchButton.expandLabel": "Expand screenshare video", - "app.switchButton.shrinkLabel": "Shrink screenshare video", - "app.sfu.mediaServerConnectionError2000": "Unable to connect to media server (error 2000)", - "app.sfu.mediaServerOffline2001": "Media server is offline. Please try again later (error 2001)", - "app.sfu.mediaServerNoResources2002": "Media server has no available resources (error 2002)", - "app.sfu.mediaServerRequestTimeout2003": "Media server requests are timing out (error 2003)", - "app.sfu.serverIceGatheringFailed2021": "Media server cannot gather connection candidates (ICE error 2021)", - "app.sfu.serverIceGatheringFailed2022": "Media server connection failed (ICE error 2022)", - "app.sfu.mediaGenericError2200": "Media server failed to process request (error 2200)", - "app.sfu.invalidSdp2202":"Client generated an invalid media request (SDP error 2202)", - "app.sfu.noAvailableCodec2203": "Server could not find an appropriate codec (error 2203)", - "app.meeting.endNotification.ok.label": "OK", - "app.whiteboard.annotations.poll": "Poll results were published", - "app.whiteboard.annotations.pollResult": "Poll Result", - "app.whiteboard.annotations.noResponses": "No responses", - "app.whiteboard.annotations.notAllowed": "You are not allowed to make this change", - "app.whiteboard.annotations.numberExceeded": "The number of annotations exceeded the limit ({0})", - "app.whiteboard.toolbar.tools": "Tools", - "app.whiteboard.toolbar.tools.hand": "Pan", - "app.whiteboard.toolbar.tools.pencil": "Pencil", - "app.whiteboard.toolbar.tools.rectangle": "Rectangle", - "app.whiteboard.toolbar.tools.triangle": "Triangle", - "app.whiteboard.toolbar.tools.ellipse": "Ellipse", - "app.whiteboard.toolbar.tools.line": "Line", - "app.whiteboard.toolbar.tools.text": "Text", - "app.whiteboard.toolbar.thickness": "Drawing thickness", - "app.whiteboard.toolbar.thicknessDisabled": "Drawing thickness is disabled", - "app.whiteboard.toolbar.color": "Colors", - "app.whiteboard.toolbar.colorDisabled": "Colors is disabled", - "app.whiteboard.toolbar.color.black": "Black", - "app.whiteboard.toolbar.color.white": "White", - "app.whiteboard.toolbar.color.red": "Red", - "app.whiteboard.toolbar.color.orange": "Orange", - "app.whiteboard.toolbar.color.eletricLime": "Electric lime", - "app.whiteboard.toolbar.color.lime": "Lime", - "app.whiteboard.toolbar.color.cyan": "Cyan", - "app.whiteboard.toolbar.color.dodgerBlue": "Dodger blue", - "app.whiteboard.toolbar.color.blue": "Blue", - "app.whiteboard.toolbar.color.violet": "Violet", - "app.whiteboard.toolbar.color.magenta": "Magenta", - "app.whiteboard.toolbar.color.silver": "Silver", - "app.whiteboard.toolbar.undo": "Undo annotation", - "app.whiteboard.toolbar.clear": "Clear all annotations", - "app.whiteboard.toolbar.clearConfirmation": "Are you sure you want to clear all annotations?", - "app.whiteboard.toolbar.multiUserOn": "Turn multi-user whiteboard on", - "app.whiteboard.toolbar.multiUserOff": "Turn multi-user whiteboard off", - "app.whiteboard.toolbar.palmRejectionOn": "Turn palm rejection on", - "app.whiteboard.toolbar.palmRejectionOff": "Turn palm rejection off", - "app.whiteboard.toolbar.fontSize": "Font size list", - "app.whiteboard.toolbarAriaLabel": "Presentation tools", - "app.feedback.title": "You have logged out of the conference", - "app.feedback.subtitle": "We'd love to hear about your experience with BigBlueButton (optional)", - "app.feedback.textarea": "How can we make BigBlueButton better?", - "app.feedback.sendFeedback": "Send Feedback", - "app.feedback.sendFeedbackDesc": "Send a feedback and leave the meeting", - "app.videoDock.webcamMirrorLabel": "Mirror", - "app.videoDock.webcamMirrorDesc": "Mirror the selected webcam", - "app.videoDock.webcamFocusLabel": "Focus", - "app.videoDock.webcamFocusDesc": "Focus the selected webcam", - "app.videoDock.webcamUnfocusLabel": "Unfocus", - "app.videoDock.webcamUnfocusDesc": "Unfocus the selected webcam", - "app.videoDock.webcamDisableLabel": "Disable self-view", - "app.videoDock.webcamEnableLabel": "Enable self-view", - "app.videoDock.webcamDisableDesc": "Self-view disabled", - "app.videoDock.webcamPinLabel": "Pin", - "app.videoDock.webcamPinDesc": "Pin the selected webcam", - "app.videoDock.webcamFullscreenLabel": "Fullscreen webcam", - "app.videoDock.webcamSqueezedButtonLabel": "Webcam options", - "app.videoDock.webcamUnpinLabel": "Unpin", - "app.videoDock.webcamUnpinLabelDisabled": "Only moderators can unpin users", - "app.videoDock.webcamUnpinDesc": "Unpin the selected webcam", - "app.videoDock.autoplayBlockedDesc": "We need your permission to show you other users' webcams.", - "app.videoDock.autoplayAllowLabel": "View webcams", - "app.createBreakoutRoom.title": "Breakout Rooms", - "app.createBreakoutRoom.ariaTitle": "Hide Breakout Rooms", - "app.createBreakoutRoom.breakoutRoomLabel": "Breakout Rooms {0}", - "app.createBreakoutRoom.askToJoin": "Ask to join", - "app.createBreakoutRoom.generatingURL": "Generating URL", - "app.createBreakoutRoom.generatingURLMessage": "We are generating a join URL for the selected breakout room. It may take a few seconds...", - "app.createBreakoutRoom.duration": "Duration {0}", - "app.createBreakoutRoom.room": "Room {0}", - "app.createBreakoutRoom.notAssigned": "Not assigned ({0})", - "app.createBreakoutRoom.join": "Join room", - "app.createBreakoutRoom.joinAudio": "Join audio", - "app.createBreakoutRoom.returnAudio": "Return audio", - "app.createBreakoutRoom.alreadyConnected": "Already in room", - "app.createBreakoutRoom.confirm": "Create", - "app.createBreakoutRoom.record": "Record", - "app.createBreakoutRoom.numberOfRooms": "Number of rooms", - "app.createBreakoutRoom.durationInMinutes": "Duration (minutes)", - "app.createBreakoutRoom.randomlyAssign": "Randomly assign", - "app.createBreakoutRoom.randomlyAssignDesc": "Assigns users randomly to breakout rooms", - "app.createBreakoutRoom.resetAssignments": "Reset assignments", - "app.createBreakoutRoom.resetAssignmentsDesc": "Reset all user room assignments", - "app.createBreakoutRoom.endAllBreakouts": "End all breakout rooms", - "app.createBreakoutRoom.chatTitleMsgAllRooms": "all rooms", - "app.createBreakoutRoom.msgToBreakoutsSent": "Message was sent to {0} breakout rooms", - "app.createBreakoutRoom.roomName": "{0} (Room - {1})", - "app.createBreakoutRoom.doneLabel": "Done", - "app.createBreakoutRoom.nextLabel": "Next", - "app.createBreakoutRoom.minusRoomTime": "Decrease breakout room time to", - "app.createBreakoutRoom.addRoomTime": "Increase breakout room time to", - "app.createBreakoutRoom.addParticipantLabel": "+ Add participant", - "app.createBreakoutRoom.freeJoin": "Allow users to choose a breakout room to join", - "app.createBreakoutRoom.captureNotes": "Capture shared notes when breakout rooms end", - "app.createBreakoutRoom.sendInvitationToMods": "Send invitation to assigned moderators", - "app.createBreakoutRoom.captureSlides": "Capture whiteboard when breakout rooms end", - "app.createBreakoutRoom.leastOneWarnBreakout": "You must place at least one user in a breakout room.", - "app.createBreakoutRoom.minimumDurationWarnBreakout": "Minimum duration for a breakout room is {0} minutes.", - "app.createBreakoutRoom.modalDesc": "Tip: You can drag-and-drop a user's name to assign them to a specific breakout room.", - "app.createBreakoutRoom.roomTime": "{0} minutes", - "app.createBreakoutRoom.numberOfRoomsError": "The number of rooms is invalid.", - "app.createBreakoutRoom.duplicatedRoomNameError": "Room name can't be duplicated.", - "app.createBreakoutRoom.emptyRoomNameError": "Room name can't be empty.", - "app.createBreakoutRoom.setTimeInMinutes": "Set duration to (minutes)", - "app.createBreakoutRoom.setTimeLabel": "Apply", - "app.createBreakoutRoom.setTimeCancel": "Cancel", - "app.createBreakoutRoom.setTimeHigherThanMeetingTimeError": "The breakout rooms duration can't exceed the meeting remaining time.", - "app.createBreakoutRoom.roomNameInputDesc": "Updates breakout room name", - "app.createBreakoutRoom.movedUserLabel": "Moved {0} to room {1}", - "app.updateBreakoutRoom.modalDesc": "To update or invite a user, simply drag them into the desired room.", - "app.updateBreakoutRoom.cancelLabel": "Cancel", - "app.updateBreakoutRoom.title": "Update Breakout Rooms", - "app.updateBreakoutRoom.confirm": "Apply", - "app.updateBreakoutRoom.userChangeRoomNotification": "You were moved to room {0}.", - "app.smartMediaShare.externalVideo": "External video(s)", - "app.update.resetRoom": "Reset user room", - "app.externalVideo.start": "Share a new video", - "app.externalVideo.title": "Share an external video", - "app.externalVideo.input": "External Video URL", - "app.externalVideo.urlInput": "Add Video URL", - "app.externalVideo.urlError": "This video URL isn't supported", - "app.externalVideo.close": "Close", - "app.externalVideo.autoPlayWarning": "Play the video to enable media synchronization", - "app.externalVideo.refreshLabel": "Refresh Video Player", - "app.externalVideo.fullscreenLabel": "Video Player", - "app.externalVideo.noteLabel": "Note: Shared external videos will not appear in the recording. YouTube, Vimeo, Instructure Media, Twitch, Dailymotion and media file URLs (e.g. https://example.com/xy.mp4) are supported.", - "app.externalVideo.subtitlesOn": "Turn off", - "app.externalVideo.subtitlesOff": "Turn on (if available)", - "app.actionsBar.actionsDropdown.shareExternalVideo": "Share an external video", - "app.actionsBar.actionsDropdown.stopShareExternalVideo": "Stop sharing external video", - "app.legacy.unsupportedBrowser": "It looks like you're using a browser that is not supported. Please use either {0} or {1} for full support.", - "app.legacy.upgradeBrowser": "It looks like you're using an older version of a supported browser. Please upgrade your browser for full support.", - "app.legacy.criosBrowser": "On iOS please use Safari for full support.", - "app.debugWindow.windowTitle": "Debug", - "app.debugWindow.form.userAgentLabel": "User Agent", - "app.debugWindow.form.button.copy": "Copy", - "app.debugWindow.form.enableAutoarrangeLayoutLabel": "Enable Auto Arrange Layout", - "app.debugWindow.form.enableAutoarrangeLayoutDescription": "(it will be disabled if you drag or resize the webcams area)", - "app.debugWindow.form.chatLoggerLabel": "Test Chat Logger Levels", - "app.debugWindow.form.button.apply": "Apply", - "app.layout.modal.title": "Layouts", - "app.layout.modal.confirm": "Confirm", - "app.layout.modal.cancel": "Cancel", - "app.layout.modal.layoutLabel": "Select your layout", - "app.layout.modal.keepPushingLayoutLabel": "Push layout to all", - "app.layout.modal.pushLayoutLabel": "Push to everyone", - "app.layout.modal.layoutToastLabel": "Layout settings changed", - "app.layout.modal.layoutSingular": "Layout", - "app.layout.modal.layoutBtnDesc": "Sets layout as selected option", - "app.layout.style.custom": "Custom", - "app.layout.style.smart": "Smart layout", - "app.layout.style.presentationFocus": "Focus on presentation", - "app.layout.style.videoFocus": "Focus on video", - "app.layout.style.customPush": "Custom (push layout to all)", - "app.layout.style.smartPush": "Smart layout (push layout to all)", - "app.layout.style.presentationFocusPush": "Focus on presentation (push layout to all)", - "app.layout.style.videoFocusPush": "Focus on video (push layout to all)", - "playback.button.about.aria": "About", - "playback.button.clear.aria": "Clear search", - "playback.button.close.aria": "Close modal", - "playback.button.fullscreen.aria": "Fullscreen content", - "playback.button.restore.aria": "Restore content", - "playback.button.search.aria": "Search", - "playback.button.section.aria": "Side section", - "playback.button.swap.aria": "Swap content", - "playback.button.theme.aria": "Toggle theme", - "playback.error.wrapper.aria": "Error area", - "playback.loader.wrapper.aria": "Loader area", - "playback.player.wrapper.aria": "Player area", - "playback.player.about.modal.shortcuts.title": "Shortcuts", - "playback.player.about.modal.shortcuts.alt": "Alt", - "playback.player.about.modal.shortcuts.shift": "Shift", - "playback.player.about.modal.shortcuts.fullscreen": "Toggle fullscreen", - "playback.player.about.modal.shortcuts.play": "Play/Pause", - "playback.player.about.modal.shortcuts.section": "Toggle side section", - "playback.player.about.modal.shortcuts.seek.backward": "Seek backwards", - "playback.player.about.modal.shortcuts.seek.forward": "Seek forwards", - "playback.player.about.modal.shortcuts.skip.next": "Next slide", - "playback.player.about.modal.shortcuts.skip.previous": "Previous Slide", - "playback.player.about.modal.shortcuts.swap": "Swap content", - "playback.player.chat.message.poll.name": "Poll result", - "playback.player.chat.message.poll.question": "Question", - "playback.player.chat.message.poll.options": "Options", - "playback.player.chat.message.poll.option.yes": "Yes", - "playback.player.chat.message.poll.option.no": "No", - "playback.player.chat.message.poll.option.abstention": "Abstention", - "playback.player.chat.message.poll.option.true": "True", - "playback.player.chat.message.poll.option.false": "False", - "playback.player.chat.message.video.name": "External video", - "playback.player.chat.wrapper.aria": "Chat area", - "playback.player.notes.wrapper.aria": "Notes area", - "playback.player.presentation.wrapper.aria": "Presentation area", - "playback.player.screenshare.wrapper.aria": "Screenshare area", - "playback.player.search.modal.title": "Search", - "playback.player.search.modal.subtitle": "Find presentation slides content", - "playback.player.thumbnails.wrapper.aria": "Thumbnails area", - "playback.player.webcams.wrapper.aria": "Webcams area", - "app.learningDashboard.dashboardTitle": "Learning Analytics Dashboard", - "app.learningDashboard.bigbluebuttonTitle": "BigBlueButton", - "app.learningDashboard.downloadSessionDataLabel": "Download Session Data", - "app.learningDashboard.lastUpdatedLabel": "Last updated at", - "app.learningDashboard.sessionDataDownloadedLabel": "Downloaded!", - "app.learningDashboard.shareButton": "Share with others", - "app.learningDashboard.shareLinkCopied": "Link successfully copied!", - "app.learningDashboard.user": "User", - "app.learningDashboard.indicators.meetingStatusEnded": "Ended", - "app.learningDashboard.indicators.meetingStatusActive": "Active", - "app.learningDashboard.indicators.usersOnline": "Active Users", - "app.learningDashboard.indicators.usersTotal": "Total Number Of Users", - "app.learningDashboard.indicators.polls": "Polls", - "app.learningDashboard.indicators.timeline": "Timeline", - "app.learningDashboard.indicators.activityScore": "Activity Score", - "app.learningDashboard.indicators.duration": "Duration", - "app.learningDashboard.userDetails.startTime": "Start Time", - "app.learningDashboard.userDetails.endTime": "End Time", - "app.learningDashboard.userDetails.joined": "Joined", - "app.learningDashboard.userDetails.category": "Category", - "app.learningDashboard.userDetails.average": "Average", - "app.learningDashboard.userDetails.activityPoints": "Activity Points", - "app.learningDashboard.userDetails.poll": "Poll", - "app.learningDashboard.userDetails.response": "Response", - "app.learningDashboard.userDetails.mostCommonAnswer": "Most Common Answer", - "app.learningDashboard.userDetails.anonymousAnswer": "Anonymous Poll", - "app.learningDashboard.userDetails.talkTime": "Talk Time", - "app.learningDashboard.userDetails.messages": "Messages", - "app.learningDashboard.userDetails.emojis": "Emojis", - "app.learningDashboard.userDetails.raiseHands": "Raise Hands", - "app.learningDashboard.userDetails.pollVotes": "Poll Votes", - "app.learningDashboard.userDetails.onlineIndicator": "{0} online time", - "app.learningDashboard.usersTable.title": "Overview", - "app.learningDashboard.usersTable.colOnline": "Online time", - "app.learningDashboard.usersTable.colTalk": "Talk time", - "app.learningDashboard.usersTable.colWebcam": "Webcam Time", - "app.learningDashboard.usersTable.colMessages": "Messages", - "app.learningDashboard.usersTable.colEmojis": "Emojis", - "app.learningDashboard.usersTable.colRaiseHands": "Raise Hands", - "app.learningDashboard.usersTable.colActivityScore": "Activity Score", - "app.learningDashboard.usersTable.colStatus": "Status", - "app.learningDashboard.usersTable.userStatusOnline": "Online", - "app.learningDashboard.usersTable.userStatusOffline": "Offline", - "app.learningDashboard.usersTable.noUsers": "No users yet", - "app.learningDashboard.usersTable.name": "Name", - "app.learningDashboard.usersTable.moderator": "Moderator", - "app.learningDashboard.usersTable.pollVotes": "Poll Votes", - "app.learningDashboard.usersTable.join": "Join", - "app.learningDashboard.usersTable.left": "Left", - "app.learningDashboard.usersTable.notAvailable": "N/A", - "app.learningDashboard.pollsTable.title": "Polls", - "app.learningDashboard.pollsTable.anonymousAnswer": "Anonymous Poll (answers in the last row)", - "app.learningDashboard.pollsTable.anonymousRowName": "Anonymous", - "app.learningDashboard.pollsTable.noPollsCreatedHeading": "No polls have been created", - "app.learningDashboard.pollsTable.noPollsCreatedMessage": "Once a poll has been sent to users, their results will appear in this list.", - "app.learningDashboard.pollsTable.answerTotal": "Total", - "app.learningDashboard.pollsTable.userLabel": "User", - "app.learningDashboard.statusTimelineTable.title": "Timeline", - "app.learningDashboard.statusTimelineTable.thumbnail": "Presentation thumbnail", - "app.learningDashboard.statusTimelineTable.presentation": "Presentation", - "app.learningDashboard.statusTimelineTable.pageNumber": "Page", - "app.learningDashboard.statusTimelineTable.setAt": "Set at", - "app.learningDashboard.errors.invalidToken": "Invalid session token", - "app.learningDashboard.errors.dataUnavailable": "Data is no longer available", - "mobileApp.portals.list.empty.addFirstPortal.label": "Add your first portal using the button above,", - "mobileApp.portals.list.empty.orUseOurDemoServer.label": "or use our demo server.", - "mobileApp.portals.list.add.button.label": "Add portal", - "mobileApp.portals.fields.name.label": "Portal Name", - "mobileApp.portals.fields.name.placeholder": "BigBlueButton demo", - "mobileApp.portals.fields.url.label": "Server URL", - "mobileApp.portals.addPortalPopup.confirm.button.label": "Save", - "mobileApp.portals.drawerNavigation.button.label": "Portals", - "mobileApp.portals.addPortalPopup.validation.emptyFields": "Required Fields", - "mobileApp.portals.addPortalPopup.validation.portalNameAlreadyExists": "Name already in use", - "mobileApp.portals.addPortalPopup.validation.urlInvalid": "Error trying to load the page - check URL and network connection" + "app.home.greeting": "Your presentation will begin shortly ...", + "app.chat.submitLabel": "Send message", + "app.chat.loading": "Chat messages loaded: {0}%", + "app.chat.errorMaxMessageLength": "The message is too long, exceeded the maximum of {0} characters", + "app.chat.disconnected": "You are disconnected, messages can't be sent", + "app.chat.locked": "Chat is locked, messages can't be sent", + "app.chat.inputLabel": "Message input for chat {0}", + "app.chat.emojiButtonLabel": "Emoji Picker", + "app.chat.loadMoreButtonLabel": "Load More", + "app.chat.inputPlaceholder": "Message {0}", + "app.chat.titlePublic": "Public Chat", + "app.chat.titlePrivate": "Private Chat with {0}", + "app.chat.partnerDisconnected": "{0} has left the meeting", + "app.chat.closeChatLabel": "Close {0}", + "app.chat.hideChatLabel": "Hide {0}", + "app.chat.moreMessages": "More messages below", + "app.chat.dropdown.options": "Chat options", + "app.chat.dropdown.clear": "Clear", + "app.chat.dropdown.copy": "Copy", + "app.chat.dropdown.save": "Save", + "app.chat.dropdown.showWelcomeMessage": "Show Welcome Message", + "app.chat.label": "Chat", + "app.chat.offline": "Offline", + "app.chat.pollResult": "Poll Results", + "app.chat.breakoutDurationUpdated": "Breakout time is now {0} minutes", + "app.chat.breakoutDurationUpdatedModerator": "Breakout rooms time is now {0} minutes, and a notification has been sent.", + "app.chat.emptyLogLabel": "Chat log empty", + "app.chat.away": "Is away", + "app.chat.notAway": "Is not away anymore", + "app.chat.clearPublicChatMessage": "The public chat history was cleared by a moderator", + "app.chat.multi.typing": "Multiple users are typing", + "app.chat.one.typing": "{0} is typing", + "app.chat.two.typing": "{0} and {1} are typing", + "app.chat.copySuccess": "Copied chat transcript", + "app.chat.copyErr": "Copy chat transcript failed", + "app.emojiPicker.search": "Search", + "app.emojiPicker.notFound": "No Emoji Found", + "app.emojiPicker.skintext": "Choose your default skin tone", + "app.emojiPicker.clear": "Clear", + "app.emojiPicker.categories.label": "Emoji categories", + "app.emojiPicker.categories.people": "People & Body", + "app.emojiPicker.categories.reactions": "Reactions", + "app.emojiPicker.categories.nature": "Animals & Nature", + "app.emojiPicker.categories.foods": "Food & Drink", + "app.emojiPicker.categories.places": "Travel & Places", + "app.emojiPicker.categories.activity": "Activity", + "app.emojiPicker.categories.objects": "Objects", + "app.emojiPicker.categories.symbols": "Symbols", + "app.emojiPicker.categories.flags": "Flags", + "app.emojiPicker.categories.recent": "Frequently Used", + "app.emojiPicker.categories.search": "Search Results", + "app.emojiPicker.skintones.1": "Default Skin Tone", + "app.emojiPicker.skintones.2": "Light Skin Tone", + "app.emojiPicker.skintones.3": "Medium-Light Skin Tone", + "app.emojiPicker.skintones.4": "Medium Skin Tone", + "app.emojiPicker.skintones.5": "Medium-Dark Skin Tone", + "app.emojiPicker.skintones.6": "Dark Skin Tone", + "app.timer.title": "Time", + "app.timer.stopwatch.title": "Stopwatch", + "app.timer.timer.title": "Timer", + "app.timer.hideTimerLabel": "Hide time", + "app.timer.button.stopwatch": "Stopwatch", + "app.timer.button.timer": "Timer", + "app.timer.button.start": "Start", + "app.timer.button.stop": "Stop", + "app.timer.button.reset": "Reset", + "app.timer.hours": "hours", + "app.timer.minutes": "minutes", + "app.timer.seconds": "seconds", + "app.timer.songs": "Songs", + "app.timer.noTrack": "No song", + "app.timer.track1": "Relaxing", + "app.timer.track2": "Calm", + "app.timer.track3": "Happy", + "app.captions.label": "Captions", + "app.captions.menu.close": "Close", + "app.captions.menu.start": "Start", + "app.captions.menu.ariaStart": "Start writing captions", + "app.captions.menu.ariaStartDesc": "Opens captions editor and closes the modal", + "app.captions.menu.select": "Select available language", + "app.captions.menu.ariaSelect": "Captions language", + "app.captions.menu.subtitle": "Please select a language and styles for closed captions within your session.", + "app.captions.menu.title": "Closed captions", + "app.captions.menu.fontSize": "Size", + "app.captions.menu.fontColor": "Text color", + "app.captions.menu.fontFamily": "Font", + "app.captions.menu.backgroundColor": "Background color", + "app.captions.menu.previewLabel": "Preview", + "app.captions.menu.cancelLabel": "Cancel", + "app.captions.hide": "Hide closed captions", + "app.captions.ownership": "Take over", + "app.captions.ownershipTooltip": "You will be assigned as the owner of {0} captions", + "app.captions.dictationStart": "Start dictation", + "app.captions.dictationStop": "Stop dictation", + "app.captions.dictationOnDesc": "Turns speech recognition on", + "app.captions.dictationOffDesc": "Turns speech recognition off", + "app.captions.speech.start": "Speech recognition started", + "app.captions.speech.stop": "Speech recognition stopped", + "app.captions.speech.error": "Speech recognition stopped due to the browser incompatibility or some time of silence", + "app.confirmation.skipConfirm": "Don't ask again", + "app.confirmation.virtualBackground.title": "Start new virtual background", + "app.confirmation.virtualBackground.description": "{0} will be added as virtual background. Continue?", + "app.confirmationModal.yesLabel": "Yes", + "app.textInput.sendLabel": "Send", + "app.title.defaultViewLabel": "Default presentation view", + "app.notes.title": "Shared Notes", + "app.notes.titlePinned": "Shared Notes (Pinned)", + "app.notes.pinnedNotification": "The Shared Notes are now pinned in the whiteboard.", + "app.notes.label": "Notes", + "app.notes.hide": "Hide notes", + "app.notes.locked": "Locked", + "app.notes.disabled": "Pinned on media area", + "app.notes.notesDropdown.covertAndUpload": "Convert notes to presentation", + "app.notes.notesDropdown.pinNotes": "Pin notes onto whiteboard", + "app.notes.notesDropdown.unpinNotes": "Unpin notes", + "app.notes.notesDropdown.notesOptions": "Notes options", + "app.pads.hint": "Press Esc to focus the pad's toolbar", + "app.user.activityCheck": "User activity check", + "app.user.activityCheck.label": "Check if user is still in meeting ({0})", + "app.user.activityCheck.check": "Check", + "app.userList.usersTitle": "Users", + "app.userList.participantsTitle": "Participants", + "app.userList.messagesTitle": "Messages", + "app.userList.notesTitle": "Notes", + "app.userList.notesListItem.unreadContent": "New content is available in the shared notes section", + "app.userList.timerTitle": "Time", + "app.userList.captionsTitle": "Captions", + "app.userList.presenter": "Presenter", + "app.userList.you": "You", + "app.userList.locked": "Locked", + "app.userList.byModerator": "by (Moderator)", + "app.userList.label": "User list", + "app.userList.toggleCompactView.label": "Toggle compact view mode", + "app.userList.moderator": "Moderator", + "app.userList.mobile": "Mobile", + "app.userList.guest": "Guest", + "app.userList.sharingWebcam": "Webcam", + "app.userList.menuTitleContext": "Available options", + "app.userList.chatListItem.unreadSingular": "One new message", + "app.userList.chatListItem.unreadPlural": "{0} new messages", + "app.userList.menu.away": "Set yourself as away", + "app.userList.menu.notAway": "Set yourself as active", + "app.userList.menu.chat.label": "Start a private chat", + "app.userList.menu.clearStatus.label": "Clear status", + "app.userList.menu.removeUser.label": "Remove user", + "app.userList.menu.removeConfirmation.label": "Remove user ({0})", + "app.userlist.menu.removeConfirmation.desc": "Prevent this user from rejoining the session.", + "app.userList.menu.muteUserAudio.label": "Mute user", + "app.userList.menu.unmuteUserAudio.label": "Unmute user", + "app.userList.menu.webcamPin.label": "Pin user's webcam", + "app.userList.menu.webcamUnpin.label": "Unpin user's webcam", + "app.userList.menu.giveWhiteboardAccess.label": "Give whiteboard access", + "app.userList.menu.removeWhiteboardAccess.label": "Remove whiteboard access", + "app.userList.menu.ejectUserCameras.label": "Close cameras", + "app.userList.userAriaLabel": "{0} {1} {2} Status {3}", + "app.userList.menu.promoteUser.label": "Promote to moderator", + "app.userList.menu.demoteUser.label": "Demote to viewer", + "app.userList.menu.unlockUser.label": "Unlock {0}", + "app.userList.menu.lockUser.label": "Lock {0}", + "app.userList.menu.directoryLookup.label": "Directory Lookup", + "app.userList.menu.makePresenter.label": "Make presenter", + "app.userList.userOptions.manageUsersLabel": "Manage users", + "app.userList.userOptions.muteAllLabel": "Mute all users", + "app.userList.userOptions.muteAllDesc": "Mutes all users in the meeting", + "app.userList.userOptions.clearAllLabel": "Clear all status icons", + "app.userList.userOptions.clearAllDesc": "Clears all status icons from users", + "app.userList.userOptions.muteAllExceptPresenterLabel": "Mute all users except presenter", + "app.userList.userOptions.muteAllExceptPresenterDesc": "Mutes all users in the meeting except the presenter", + "app.userList.userOptions.unmuteAllLabel": "Turn off meeting mute", + "app.userList.userOptions.unmuteAllDesc": "Unmutes the meeting", + "app.userList.userOptions.lockViewersLabel": "Lock viewers", + "app.userList.userOptions.lockViewersDesc": "Lock certain functionalities for attendees of the meeting", + "app.userList.userOptions.guestPolicyLabel": "Guest policy", + "app.userList.userOptions.guestPolicyDesc": "Change meeting guest policy setting", + "app.userList.userOptions.disableCam": "Viewers' webcams are disabled", + "app.userList.userOptions.disableMic": "Viewers' microphones are disabled", + "app.userList.userOptions.disablePrivChat": "Private chat is disabled", + "app.userList.userOptions.disablePubChat": "Public chat is disabled", + "app.userList.userOptions.disableNotes": "Shared notes are now locked", + "app.userList.userOptions.hideUserList": "User list is now hidden for viewers", + "app.userList.userOptions.webcamsOnlyForModerator": "Only moderators are able to see viewers' webcams (due to lock settings)", + "app.userList.content.participants.options.clearedStatus": "Cleared all user status", + "app.userList.userOptions.enableCam": "Viewers' webcams are enabled", + "app.userList.userOptions.enableMic": "Viewers' microphones are enabled", + "app.userList.userOptions.enablePrivChat": "Private chat is enabled", + "app.userList.userOptions.enablePubChat": "Public chat is enabled", + "app.userList.userOptions.enableNotes": "Shared notes are now enabled", + "app.userList.userOptions.showUserList": "User list is now shown to viewers", + "app.userList.userOptions.enableOnlyModeratorWebcam": "You can enable your webcam now, everyone will see you", + "app.userList.userOptions.savedNames.title": "List of users in meeting {0} at {1}", + "app.userList.userOptions.sortedFirstName.heading": "Sorted by first name:", + "app.userList.userOptions.sortedLastName.heading": "Sorted by last name:", + "app.userList.userOptions.hideViewersCursor": "Viewer cursors are locked", + "app.userList.userOptions.showViewersCursor": "Viewer cursors are unlocked", + "app.media.label": "Media", + "app.media.autoplayAlertDesc": "Allow Access", + "app.media.screenshare.start": "Screenshare has started", + "app.media.screenshare.end": "Screenshare has ended", + "app.media.screenshare.endDueToDataSaving": "Screenshare stopped due to data savings", + "app.media.screenshare.unavailable": "Screenshare Unavailable", + "app.media.screenshare.notSupported": "Screensharing is not supported in this browser.", + "app.media.screenshare.autoplayBlockedDesc": "We need your permission to show you the presenter's screen.", + "app.media.screenshare.autoplayAllowLabel": "View shared screen", + "app.media.cameraAsContent.start": "Present camera has started", + "app.media.cameraAsContent.end": "Present camera has ended", + "app.media.cameraAsContent.endDueToDataSaving": "Present camera stopped due to data savings", + "app.media.cameraAsContent.autoplayBlockedDesc": "We need your permission to show you the presenter's camera.", + "app.media.cameraAsContent.autoplayAllowLabel": "View present camera", + "app.screenshare.presenterLoadingLabel": "Your screenshare is loading", + "app.screenshare.viewerLoadingLabel": "The presenter's screen is loading", + "app.screenshare.presenterSharingLabel": "You are now sharing your screen", + "app.screenshare.screenshareFinalError": "Code {0}. Could not share the screen.", + "app.screenshare.screenshareRetryError": "Code {0}. Try sharing the screen again.", + "app.screenshare.screenshareRetryOtherEnvError": "Code {0}. Could not share the screen. Try again using a different browser or device.", + "app.screenshare.screenshareUnsupportedEnv": "Code {0}. Browser is not supported. Try again using a different browser or device.", + "app.screenshare.screensharePermissionError": "Code {0}. Permission to capture the screen needs to be granted.", + "app.cameraAsContent.presenterLoadingLabel": "Your camera is loading", + "app.cameraAsContent.viewerLoadingLabel": "The presenter's camera is loading", + "app.cameraAsContent.presenterSharingLabel": "You are now presenting your camera", + "app.meeting.ended": "This session has ended", + "app.meeting.meetingTimeRemaining": "Meeting time remaining: {0}", + "app.meeting.meetingTimeHasEnded": "Time ended. Meeting will close soon", + "app.meeting.endedByUserMessage": "This session was ended by {0}", + "app.meeting.endedByNoModeratorMessageSingular": "The meeting has ended because no moderator has been present for one minute", + "app.meeting.endedByNoModeratorMessagePlural": "The meeting has ended because no moderator has been present for {0} minutes", + "app.meeting.endedMessage": "You will be forwarded back to the home screen", + "app.meeting.alertMeetingEndsUnderMinutesSingular": "Meeting is closing in one minute.", + "app.meeting.alertMeetingEndsUnderMinutesPlural": "Meeting is closing in {0} minutes.", + "app.meeting.alertBreakoutEndsUnderMinutesPlural": "Breakout is closing in {0} minutes.", + "app.meeting.alertBreakoutEndsUnderMinutesSingular": "Breakout is closing in one minute.", + "app.presentation.hide": "Hide presentation", + "app.presentation.notificationLabel": "Current presentation", + "app.presentation.downloadLabel": "Download", + "app.presentation.slideContent": "Slide Content", + "app.presentation.startSlideContent": "Slide content start", + "app.presentation.endSlideContent": "Slide content end", + "app.presentation.changedSlideContent": "Presentation changed to slide: {0}", + "app.presentation.emptySlideContent": "No content for current slide", + "app.presentation.options.fullscreen": "Fullscreen Presentation", + "app.presentation.options.exitFullscreen": "Exit Fullscreen", + "app.presentation.options.minimize": "Minimize", + "app.presentation.options.snapshot": "Snapshot of current slide", + "app.presentation.options.downloading": "Downloading...", + "app.presentation.options.downloaded": "Current presentation was downloaded", + "app.presentation.options.downloadFailed": "Could not download current presentation", + "app.presentation.presentationToolbar.noNextSlideDesc": "End of presentation", + "app.presentation.presentationToolbar.noPrevSlideDesc": "Start of presentation", + "app.presentation.presentationToolbar.selectLabel": "Select slide", + "app.presentation.presentationToolbar.prevSlideLabel": "Previous slide", + "app.presentation.presentationToolbar.prevSlideDesc": "Change the presentation to the previous slide", + "app.presentation.presentationToolbar.nextSlideLabel": "Next slide", + "app.presentation.presentationToolbar.nextSlideDesc": "Change the presentation to the next slide", + "app.presentation.presentationToolbar.skipSlideLabel": "Skip slide", + "app.presentation.presentationToolbar.skipSlideDesc": "Change the presentation to a specific slide", + "app.presentation.presentationToolbar.fitWidthLabel": "Fit to width", + "app.presentation.presentationToolbar.fitWidthDesc": "Display the whole width of the slide", + "app.presentation.presentationToolbar.fitScreenLabel": "Fit to screen", + "app.presentation.presentationToolbar.fitScreenDesc": "Display the whole slide", + "app.presentation.presentationToolbar.zoomLabel": "Zoom", + "app.presentation.presentationToolbar.zoomDesc": "Change the zoom level of the presentation", + "app.presentation.presentationToolbar.zoomInLabel": "Zoom in", + "app.presentation.presentationToolbar.zoomInDesc": "Zoom in the presentation", + "app.presentation.presentationToolbar.zoomOutLabel": "Zoom out", + "app.presentation.presentationToolbar.zoomOutDesc": "Zoom out of the presentation", + "app.presentation.presentationToolbar.zoomReset": "Reset Zoom", + "app.presentation.presentationToolbar.zoomIndicator": "Current zoom percentage", + "app.presentation.presentationToolbar.fitToWidth": "Fit to width", + "app.presentation.presentationToolbar.fitToPage": "Fit to page", + "app.presentation.presentationToolbar.goToSlide": "Slide {0}", + "app.presentation.presentationToolbar.hideToolsDesc": "Hide Toolbars", + "app.presentation.presentationToolbar.showToolsDesc": "Show Toolbars", + "app.presentation.placeholder": "There is no currently active presentation", + "app.presentationUploder.title": "Presentation", + "app.presentationUploder.message": "As a presenter you have the ability to upload any Office document or PDF file. We recommend PDF file for best results. Please ensure that a presentation is selected using the circle checkbox on the left hand side.", + "app.presentationUploader.exportHint": "Selecting \"Send to chat\" will provide users with a downloadable link with annotations in public chat.", + "app.presentationUploader.exportToastHeader": "Sending to chat ({0} item)", + "app.presentationUploader.exportToastHeaderPlural": "Sending to chat ({0} items)", + "app.presentationUploader.exporting": "Sending to chat", + "app.presentationUploader.sending": "Sending...", + "app.presentationUploader.collecting": "Extracting slide {0} of {1}...", + "app.presentationUploader.processing": "Annotating slide {0} of {1}...", + "app.presentationUploader.sent": "Sent", + "app.presentationUploader.exportingTimeout": "The export is taking too long...", + "app.presentationUploader.export": "Send to chat", + "app.presentationUploader.exportCurrentStatePresentation": "Send out a download link for the presentation in the current state of it", + "app.presentationUploader.enableOriginalPresentationDownload": "Enable download of the original presentation", + "app.presentationUploader.disableOriginalPresentationDownload": "Disable download of the original presentation", + "app.presentationUploader.dropdownExportOptions": "Export options", + "app.presentationUploader.export.linkAvailable": "Link for downloading {0} available on the public chat.", + "app.presentationUploader.export.downloadButtonAvailable": "Download button for presentation {0} is available.", + "app.presentationUploader.export.notAccessibleWarning": "may not be accessibility compliant", + "app.presentationUploader.export.originalLabel": "Original", + "app.presentationUploader.export.inCurrentStateLabel": "In current state", + "app.presentationUploader.currentPresentationLabel": "Current presentation", + "app.presentationUploder.extraHint": "IMPORTANT: each file may not exceed {0} MB and {1} pages.", + "app.presentationUploder.uploadLabel": "Upload", + "app.presentationUploder.confirmLabel": "Confirm", + "app.presentationUploder.confirmDesc": "Save your changes and start the presentation", + "app.presentationUploder.dismissLabel": "Cancel", + "app.presentationUploder.dismissDesc": "Close the modal window and discard your changes", + "app.presentationUploder.dropzoneLabel": "Drag files here to upload", + "app.presentationUploder.dropzoneImagesLabel": "Drag images here to upload", + "app.presentationUploder.browseFilesLabel": "or browse for files", + "app.presentationUploder.browseImagesLabel": "or browse/capture for images", + "app.presentationUploder.externalUploadTitle": "Add content from 3rd party application", + "app.presentationUploder.externalUploadLabel": "Browse files", + "app.presentationUploder.fileToUpload": "To be uploaded ...", + "app.presentationUploder.currentBadge": "Current", + "app.presentationUploder.rejectedError": "The selected file(s) have been rejected. Please check the file type(s).", + "app.presentationUploder.connectionClosedError": "Interrupted by poor connectivity. Please try again.", + "app.presentationUploder.upload.progress": "Uploading ({0}%)", + "app.presentationUploder.conversion.204": "No content to capture", + "app.presentationUploder.upload.413": "File is too large, exceeded the maximum of {0} MB", + "app.presentationUploder.genericError": "Oops, Something went wrong ...", + "app.presentationUploder.upload.408": "Request upload token timeout.", + "app.presentationUploder.upload.404": "404: Invalid upload token", + "app.presentationUploder.upload.401": "Request presentation upload token failed.", + "app.presentationUploder.conversion.conversionProcessingSlides": "Processing page {0} of {1}", + "app.presentationUploder.conversion.genericConversionStatus": "Converting file ...", + "app.presentationUploder.conversion.generatingThumbnail": "Generating thumbnails ...", + "app.presentationUploder.conversion.generatedSlides": "Slides generated ...", + "app.presentationUploder.conversion.generatingSvg": "Generating SVG images ...", + "app.presentationUploder.conversion.pageCountExceeded": "Number of pages exceeded maximum of {0}", + "app.presentationUploder.conversion.invalidMimeType": "Invalid format detected (extension={0}, content type={1})", + "app.presentationUploder.conversion.conversionTimeout": "Slide {0} could not be processed within {1} attempts.", + "app.presentationUploder.conversion.officeDocConversionInvalid": "Failed to process Office document. Please upload a PDF instead.", + "app.presentationUploder.conversion.officeDocConversionFailed": "Failed to process Office document. Please upload a PDF instead.", + "app.presentationUploder.conversion.pdfHasBigPage": "We could not convert the PDF file, please try optimizing it. Max page size {0}", + "app.presentationUploder.conversion.timeout": "Ops, the conversion took too long", + "app.presentationUploder.conversion.pageCountFailed": "Failed to determine the number of pages.", + "app.presentationUploder.conversion.unsupportedDocument": "File extension not supported", + "app.presentationUploder.removePresentationLabel": "Remove presentation", + "app.presentationUploder.setAsCurrentPresentation": "Set presentation as current", + "app.presentationUploder.tableHeading.filename": "File name", + "app.presentationUploder.tableHeading.options": "Options", + "app.presentationUploder.tableHeading.status": "Status", + "app.presentationUploder.uploading": "Uploading {0} {1}", + "app.presentationUploder.uploadStatus": "{0} of {1} uploads complete", + "app.presentationUploder.completed": "{0} uploads complete", + "app.presentationUploder.item": "item", + "app.presentationUploder.itemPlural": "items", + "app.presentationUploder.clearErrors": "Clear errors", + "app.presentationUploder.clearErrorsDesc": "Clears failed presentation uploads", + "app.presentationUploder.uploadViewTitle": "Upload Presentation", + "app.poll.questionAndoptions.label": "Question text to be shown.\nA. Poll option *\nB. Poll option (optional)\nC. Poll option (optional)\nD. Poll option (optional)\nE. Poll option (optional)", + "app.poll.customInput.label": "Custom Input", + "app.poll.customInputInstructions.label": "Custom input is enabled – write poll question and option(s) in given format or drag and drop a text file in same format.", + "app.poll.maxOptionsWarning.label": "Only first 5 options can be used!", + "app.poll.pollPaneTitle": "Polling", + "app.poll.enableMultipleResponseLabel": "Allow multiple answers per respondent?", + "app.poll.quickPollTitle": "Quick Poll", + "app.poll.hidePollDesc": "Hides the poll menu pane", + "app.poll.quickPollInstruction": "Select an option below to start your poll.", + "app.poll.activePollInstruction": "Leave this panel open to see live responses to your poll. When you are ready, select 'Publish polling results' to publish the results and end the poll.", + "app.poll.dragDropPollInstruction": "To fill the poll values, drag a text file with the poll values onto the highlighted field", + "app.poll.customPollTextArea": "Fill poll values", + "app.poll.publishLabel": "Publish poll", + "app.poll.cancelPollLabel": "Cancel", + "app.poll.backLabel": "Start A Poll", + "app.poll.closeLabel": "Close", + "app.poll.waitingLabel": "Waiting for responses ({0}/{1})", + "app.poll.ariaInputCount": "Custom poll option {0} of {1}", + "app.poll.customPlaceholder": "Add poll option", + "app.poll.noPresentationSelected": "No presentation selected! Please select one.", + "app.poll.clickHereToSelect": "Click here to select", + "app.poll.question.label": "Write your question...", + "app.poll.optionalQuestion.label": "Write your question (optional)...", + "app.poll.userResponse.label": "Typed Response", + "app.poll.responseTypes.label": "Response Types", + "app.poll.optionDelete.label": "Delete", + "app.poll.responseChoices.label": "Response Choices", + "app.poll.typedResponse.desc": "Users will be presented with a text box to fill in their response.", + "app.poll.addItem.label": "Add item", + "app.poll.start.label": "Start Poll", + "app.poll.secretPoll.label": "Anonymous Poll", + "app.poll.secretPoll.isSecretLabel": "The poll is anonymous – you will not be able to see individual responses.", + "app.poll.questionErr": "Providing a question is required.", + "app.poll.optionErr": "Enter a Poll option", + "app.poll.startPollDesc": "Begins the poll", + "app.poll.showRespDesc": "Displays response configuration", + "app.poll.addRespDesc": "Adds poll response input", + "app.poll.deleteRespDesc": "Removes option {0}", + "app.poll.t": "True", + "app.poll.f": "False", + "app.poll.tf": "True / False", + "app.poll.y": "Yes", + "app.poll.n": "No", + "app.poll.abstention": "Abstention", + "app.poll.yna": "Yes / No / Abstention", + "app.poll.a2": "A / B", + "app.poll.a3": "A / B / C", + "app.poll.a4": "A / B / C / D", + "app.poll.a5": "A / B / C / D / E", + "app.poll.answer.true": "True", + "app.poll.answer.false": "False", + "app.poll.answer.yes": "Yes", + "app.poll.answer.no": "No", + "app.poll.answer.abstention": "Abstention", + "app.poll.answer.a": "A", + "app.poll.answer.b": "B", + "app.poll.answer.c": "C", + "app.poll.answer.d": "D", + "app.poll.answer.e": "E", + "app.poll.liveResult.usersTitle": "Users", + "app.poll.liveResult.responsesTitle": "Response", + "app.poll.liveResult.secretLabel": "This is an anonymous poll. Individual responses are not shown.", + "app.poll.removePollOpt": "Removed Poll option {0}", + "app.poll.emptyPollOpt": "Blank", + "app.polling.pollingTitle": "Polling options", + "app.polling.pollQuestionTitle": "Polling Question", + "app.polling.submitLabel": "Submit", + "app.polling.submitAriaLabel": "Submit poll response", + "app.polling.responsePlaceholder": "Enter answer", + "app.polling.responseSecret": "Anonymous poll – the presenter can't see your answer.", + "app.polling.responseNotSecret": "Normal poll – the presenter can see your answer.", + "app.polling.pollAnswerLabel": "Poll answer {0}", + "app.polling.pollAnswerDesc": "Select this option to vote for {0}", + "app.failedMessage": "Apologies, trouble connecting to the server.", + "app.downloadPresentationButton.label": "Download the original presentation", + "app.connectingMessage": "Connecting ...", + "app.waitingMessage": "Disconnected. Trying to reconnect in {0} seconds ...", + "app.retryNow": "Retry now", + "app.muteWarning.label": "Click {0} to unmute yourself.", + "app.muteWarning.disableMessage": "Mute alerts disabled until unmute", + "app.muteWarning.tooltip": "Click to close and disable warning until next unmute", + "app.navBar.settingsDropdown.optionsLabel": "Options", + "app.navBar.settingsDropdown.fullscreenLabel": "Fullscreen Application", + "app.navBar.settingsDropdown.settingsLabel": "Settings", + "app.navBar.settingsDropdown.aboutLabel": "About", + "app.navBar.settingsDropdown.leaveSessionLabel": "Leave meeting", + "app.navBar.settingsDropdown.exitFullscreenLabel": "Exit Fullscreen", + "app.navBar.settingsDropdown.fullscreenDesc": "Make the settings menu fullscreen", + "app.navBar.settingsDropdown.settingsDesc": "Change the general settings", + "app.navBar.settingsDropdown.aboutDesc": "Show information about the client", + "app.navBar.settingsDropdown.leaveSessionDesc": "Leave the meeting", + "app.navBar.settingsDropdown.exitFullscreenDesc": "Exit fullscreen mode", + "app.navBar.settingsDropdown.hotkeysLabel": "Keyboard shortcuts", + "app.navBar.settingsDropdown.hotkeysDesc": "Listing of available keyboard shortcuts", + "app.navBar.settingsDropdown.helpLabel": "Help", + "app.navBar.settingsDropdown.openAppLabel": "Open in BigBlueButton Tablet app", + "app.navBar.settingsDropdown.helpDesc": "Links user to video tutorials (opens new tab)", + "app.navBar.settingsDropdown.endMeetingDesc": "Terminates the current meeting", + "app.navBar.settingsDropdown.endMeetingLabel": "End meeting", + "app.navBar.userListToggleBtnLabel": "User list toggle", + "app.navBar.toggleUserList.ariaLabel": "Users and messages toggle", + "app.navBar.toggleUserList.newMessages": "with new message notification", + "app.navBar.toggleUserList.newMsgAria": "New message from {0}", + "app.navBar.recording": "This session is being recorded", + "app.navBar.recording.on": "Recording", + "app.navBar.recording.off": "Not recording", + "app.navBar.emptyAudioBrdige": "No active microphone. Share your microphone to add audio to this recording.", + "app.leaveConfirmation.confirmLabel": "Leave", + "app.leaveConfirmation.confirmDesc": "Logs you out of the meeting", + "app.endMeeting.title": "End {0}", + "app.endMeeting.description": "This action will end the session for {0} active user(s). Are you sure you want to end this session?", + "app.endMeeting.noUserDescription": "Are you sure you want to end this session?", + "app.endMeeting.contentWarning": "Chat messages, shared notes, whiteboard content and shared documents for this session will no longer be directly accessible", + "app.endMeeting.yesLabel": "End session for all users", + "app.endMeeting.noLabel": "No", + "app.about.title": "About", + "app.about.version": "Client build:", + "app.about.version_label": "BigBlueButton version:", + "app.about.copyright": "Copyright:", + "app.about.confirmLabel": "OK", + "app.about.confirmDesc": "OK", + "app.about.dismissLabel": "Cancel", + "app.about.dismissDesc": "Close about client information", + "app.mobileAppModal.title": "Open BigBlueButton Tablet app", + "app.mobileAppModal.description": "Do you have the BigBlueButton Tablet app installed on your device?", + "app.mobileAppModal.openApp": "Yes, open the app now", + "app.mobileAppModal.obtainUrlMsg": "Obtaining meeting URL", + "app.mobileAppModal.obtainUrlErrorMsg": "Error trying to obtain meeting URL", + "app.mobileAppModal.openStore": "No, open the App Store to download", + "app.mobileAppModal.dismissLabel": "Cancel", + "app.mobileAppModal.dismissDesc": "Close", + "app.mobileAppModal.userConnectedWithSameId": "The user {0} just connected using the same ID as you.", + "app.actionsBar.changeStatusLabel": "Change status", + "app.actionsBar.muteLabel": "Mute", + "app.actionsBar.unmuteLabel": "Unmute", + "app.actionsBar.camOffLabel": "Camera off", + "app.actionsBar.raiseLabel": "Raise", + "app.actionsBar.label": "Actions bar", + "app.actionsBar.actionsDropdown.restorePresentationLabel": "Restore presentation", + "app.actionsBar.actionsDropdown.restorePresentationDesc": "Button to restore presentation after it has been minimized", + "app.actionsBar.actionsDropdown.minimizePresentationLabel": "Minimize presentation", + "app.actionsBar.actionsDropdown.minimizePresentationDesc": "Button used to minimize presentation", + "app.actionsBar.actionsDropdown.layoutModal": "Layout Settings Modal", + "app.actionsBar.actionsDropdown.shareCameraAsContent": "Share camera as content", + "app.actionsBar.actionsDropdown.unshareCameraAsContent": "Stop camera as content", + "app.screenshare.screenShareLabel": "Screen share", + "app.cameraAsContent.cameraAsContentLabel": "Present camera", + "app.submenu.application.applicationSectionTitle": "Application", + "app.submenu.application.animationsLabel": "Animations", + "app.submenu.application.audioFilterLabel": "Audio Filters for Microphone", + "app.submenu.application.wbToolbarsAutoHideLabel": "Auto Hide Whiteboard Toolbars", + "app.submenu.application.darkThemeLabel": "Dark mode", + "app.submenu.application.fontSizeControlLabel": "Font size", + "app.submenu.application.increaseFontBtnLabel": "Increase application font size", + "app.submenu.application.decreaseFontBtnLabel": "Decrease application font size", + "app.submenu.application.currentSize": "currently {0}", + "app.submenu.application.languageLabel": "Application Language", + "app.submenu.application.languageOptionLabel": "Choose language", + "app.submenu.application.noLocaleOptionLabel": "No active locales", + "app.submenu.application.paginationEnabledLabel": "Video pagination", + "app.submenu.application.layoutOptionLabel": "Layout type", + "app.submenu.application.pushLayoutLabel": "Push layout", + "app.submenu.application.localeDropdown.af": "Afrikaans", + "app.submenu.application.localeDropdown.ar": "Arabic", + "app.submenu.application.localeDropdown.az": "Azerbaijani", + "app.submenu.application.localeDropdown.bg-BG": "Bulgarian", + "app.submenu.application.localeDropdown.bn": "Bengali", + "app.submenu.application.localeDropdown.ca": "Catalan", + "app.submenu.application.localeDropdown.cs-CZ": "Czech", + "app.submenu.application.localeDropdown.da": "Danish", + "app.submenu.application.localeDropdown.de": "German", + "app.submenu.application.localeDropdown.dv": "Dhivehi", + "app.submenu.application.localeDropdown.el-GR": "Greek (Greece)", + "app.submenu.application.localeDropdown.en": "English", + "app.submenu.application.localeDropdown.eo": "Esperanto", + "app.submenu.application.localeDropdown.es": "Spanish", + "app.submenu.application.localeDropdown.es-419": "Spanish (Latin America)", + "app.submenu.application.localeDropdown.es-ES": "Spanish (Spain)", + "app.submenu.application.localeDropdown.es-MX": "Spanish (Mexico)", + "app.submenu.application.localeDropdown.et": "Estonian", + "app.submenu.application.localeDropdown.eu": "Basque", + "app.submenu.application.localeDropdown.fa-IR": "Persian", + "app.submenu.application.localeDropdown.fi": "Finnish", + "app.submenu.application.localeDropdown.fr": "French", + "app.submenu.application.localeDropdown.gl": "Galician", + "app.submenu.application.localeDropdown.he": "Hebrew", + "app.submenu.application.localeDropdown.hi-IN": "Hindi", + "app.submenu.application.localeDropdown.hr": "Croatian", + "app.submenu.application.localeDropdown.hu-HU": "Hungarian", + "app.submenu.application.localeDropdown.hy": "Armenian", + "app.submenu.application.localeDropdown.id": "Indonesian", + "app.submenu.application.localeDropdown.it-IT": "Italian", + "app.submenu.application.localeDropdown.ja": "Japanese", + "app.submenu.application.localeDropdown.ka": "Georgian", + "app.submenu.application.localeDropdown.km": "Khmer", + "app.submenu.application.localeDropdown.kn": "Kannada", + "app.submenu.application.localeDropdown.ko-KR": "Korean (Korea)", + "app.submenu.application.localeDropdown.lo-LA": "Lao", + "app.submenu.application.localeDropdown.lt-LT": "Lithuanian", + "app.submenu.application.localeDropdown.lv": "Latvian", + "app.submenu.application.localeDropdown.ml": "Malayalam", + "app.submenu.application.localeDropdown.mn-MN": "Mongolian", + "app.submenu.application.localeDropdown.nb-NO": "Norwegian (bokmal)", + "app.submenu.application.localeDropdown.nl": "Dutch", + "app.submenu.application.localeDropdown.oc": "Occitan", + "app.submenu.application.localeDropdown.pl-PL": "Polish", + "app.submenu.application.localeDropdown.pt": "Portuguese", + "app.submenu.application.localeDropdown.pt-BR": "Portuguese (Brazil)", + "app.submenu.application.localeDropdown.ro-RO": "Romanian", + "app.submenu.application.localeDropdown.ru": "Russian", + "app.submenu.application.localeDropdown.sk-SK": "Slovak (Slovakia)", + "app.submenu.application.localeDropdown.sl": "Slovenian", + "app.submenu.application.localeDropdown.sr": "Serbian", + "app.submenu.application.localeDropdown.sv-SE": "Swedish", + "app.submenu.application.localeDropdown.ta": "Tamil", + "app.submenu.application.localeDropdown.te": "Telugu", + "app.submenu.application.localeDropdown.th": "Thai", + "app.submenu.application.localeDropdown.tr": "Turkish", + "app.submenu.application.localeDropdown.tr-TR": "Turkish (Turkey)", + "app.submenu.application.localeDropdown.uk-UA": "Ukrainian", + "app.submenu.application.localeDropdown.vi": "Vietnamese", + "app.submenu.application.localeDropdown.vi-VN": "Vietnamese (Vietnam)", + "app.submenu.application.localeDropdown.zh-CN": "Chinese Simplified (China)", + "app.submenu.application.localeDropdown.zh-TW": "Chinese Traditional (Taiwan)", + "app.submenu.notification.SectionTitle": "Notifications", + "app.submenu.notification.Desc": "Define how and what you will be notified.", + "app.submenu.notification.audioAlertLabel": "Audio Alerts", + "app.submenu.notification.pushAlertLabel": "Popup Alerts", + "app.submenu.notification.messagesLabel": "Chat Message", + "app.submenu.notification.userJoinLabel": "User Join", + "app.submenu.notification.userLeaveLabel": "User Leave", + "app.submenu.notification.guestWaitingLabel": "Guest Waiting Approval", + "app.submenu.audio.micSourceLabel": "Microphone source", + "app.submenu.audio.speakerSourceLabel": "Speaker source", + "app.submenu.audio.streamVolumeLabel": "Your audio stream volume", + "app.submenu.video.title": "Video", + "app.submenu.video.videoSourceLabel": "View source", + "app.submenu.video.videoOptionLabel": "Choose view source", + "app.submenu.video.videoQualityLabel": "Video quality", + "app.submenu.video.qualityOptionLabel": "Choose the video quality", + "app.submenu.video.participantsCamLabel": "Viewing participants webcams", + "app.settings.applicationTab.label": "Application", + "app.settings.audioTab.label": "Audio", + "app.settings.videoTab.label": "Video", + "app.settings.usersTab.label": "Participants", + "app.settings.main.label": "Settings", + "app.settings.main.cancel.label": "Cancel", + "app.settings.main.cancel.label.description": "Discards the changes and closes the settings menu", + "app.settings.main.save.label": "Save", + "app.settings.main.save.label.description": "Saves the changes and closes the settings menu", + "app.settings.dataSavingTab.label": "Data savings", + "app.settings.dataSavingTab.webcam": "Enable other participants webcams", + "app.settings.dataSavingTab.screenShare": "Enable other participants desktop sharing", + "app.settings.dataSavingTab.description": "To save your bandwidth adjust what's currently being displayed.", + "app.settings.save-notification.label": "Settings have been saved", + "app.statusNotifier.lowerHands": "Lower Hands", + "app.statusNotifier.lowerHandDescOneUser": "Lower {0}'s hand", + "app.statusNotifier.raisedHandsTitle": "Raised Hands", + "app.statusNotifier.raisedHandDesc": "{0} raised their hands", + "app.statusNotifier.raisedHandDescOneUser": "{0} raised hand", + "app.statusNotifier.and": "and", + "app.switch.onLabel": "ON", + "app.switch.offLabel": "OFF", + "app.talkingIndicator.ariaMuteDesc": "Select to mute user", + "app.talkingIndicator.isTalking": "{0} is talking", + "app.talkingIndicator.moreThanMaxIndicatorsTalking": "{0}+ are talking", + "app.talkingIndicator.moreThanMaxIndicatorsWereTalking": "{0}+ were talking", + "app.talkingIndicator.wasTalking": "{0} stopped talking", + "app.actionsBar.actionsDropdown.actionsLabel": "Actions", + "app.actionsBar.actionsDropdown.activateTimerLabel": "Activate stopwatch", + "app.actionsBar.actionsDropdown.deactivateTimerLabel": "Deactivate stopwatch", + "app.actionsBar.actionsDropdown.presentationLabel": "Upload/Manage presentations", + "app.actionsBar.actionsDropdown.initPollLabel": "Initiate a poll", + "app.actionsBar.actionsDropdown.desktopShareLabel": "Share your screen", + "app.actionsBar.actionsDropdown.stopDesktopShareLabel": "Stop sharing your screen", + "app.actionsBar.actionsDropdown.presentationDesc": "Upload your presentation", + "app.actionsBar.actionsDropdown.initPollDesc": "Initiate a poll", + "app.actionsBar.actionsDropdown.desktopShareDesc": "Share your screen with others", + "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Stop sharing your screen with", + "app.actionsBar.actionsDropdown.pollBtnLabel": "Start a poll", + "app.actionsBar.actionsDropdown.pollBtnDesc": "Toggles poll pane", + "app.actionsBar.actionsDropdown.saveUserNames": "Save user names", + "app.actionsBar.actionsDropdown.createBreakoutRoom": "Create breakout rooms", + "app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "create breakouts for split the current meeting ", + "app.actionsBar.actionsDropdown.captionsLabel": "Write closed captions", + "app.actionsBar.actionsDropdown.captionsDesc": "Toggles captions pane", + "app.actionsBar.actionsDropdown.takePresenter": "Take presenter", + "app.actionsBar.actionsDropdown.takePresenterDesc": "Assign yourself as the new presenter", + "app.actionsBar.actionsDropdown.selectRandUserLabel": "Select random user", + "app.actionsBar.actionsDropdown.selectRandUserDesc": "Chooses a user from available viewers at random", + "app.actionsBar.actionsDropdown.propagateLayoutLabel": "Propagate layout", + "app.actionsBar.interactions.interactions": "Interactions", + "app.actionsBar.interactions.raiseHand": "Raise your hand", + "app.actionsBar.interactions.lowHand": "Lower your hand", + "app.actionsBar.interactions.addReaction": "Add a reaction", + "app.actionsBar.emojiMenu.statusTriggerLabel": "Set status", + "app.actionsBar.emojiMenu.awayLabel": "Away", + "app.actionsBar.emojiMenu.awayDesc": "Change your status to away", + "app.actionsBar.emojiMenu.raiseHandLabel": "Raise hand", + "app.actionsBar.emojiMenu.lowerHandLabel": "Lower hand", + "app.actionsBar.emojiMenu.raiseHandDesc": "Raise your hand to ask a question", + "app.actionsBar.emojiMenu.neutralLabel": "Undecided", + "app.actionsBar.emojiMenu.neutralDesc": "Change your status to undecided", + "app.actionsBar.emojiMenu.confusedLabel": "Confused", + "app.actionsBar.emojiMenu.confusedDesc": "Change your status to confused", + "app.actionsBar.emojiMenu.sadLabel": "Sad", + "app.actionsBar.emojiMenu.sadDesc": "Change your status to sad", + "app.actionsBar.emojiMenu.happyLabel": "Happy", + "app.actionsBar.emojiMenu.happyDesc": "Change your status to happy", + "app.actionsBar.emojiMenu.noneLabel": "Clear Status", + "app.actionsBar.emojiMenu.noneDesc": "Clear your status", + "app.actionsBar.emojiMenu.applauseLabel": "Applaud", + "app.actionsBar.emojiMenu.applauseDesc": "Change your status to applause", + "app.actionsBar.emojiMenu.thumbsUpLabel": "Thumbs up", + "app.actionsBar.emojiMenu.thumbsUpDesc": "Change your status to thumbs up", + "app.actionsBar.emojiMenu.thumbsDownLabel": "Thumbs down", + "app.actionsBar.emojiMenu.thumbsDownDesc": "Change your status to thumbs down", + "app.actionsBar.currentStatusDesc": "current status {0}", + "app.actionsBar.captions.start": "Start viewing closed captions", + "app.actionsBar.captions.stop": "Stop viewing closed captions", + "app.audioNotification.audioFailedError1001": "WebSocket disconnected (error 1001)", + "app.audioNotification.audioFailedError1002": "Could not make a WebSocket connection (error 1002)", + "app.audioNotification.audioFailedError1003": "Browser version not supported (error 1003)", + "app.audioNotification.audioFailedError1004": "Failure on call (reason={0}) (error 1004)", + "app.audioNotification.audioFailedError1005": "Call ended unexpectedly (error 1005)", + "app.audioNotification.audioFailedError1006": "Call timed out (error 1006)", + "app.audioNotification.audioFailedError1007": "Connection failure (ICE error 1007)", + "app.audioNotification.audioFailedError1008": "Transfer failed (error 1008)", + "app.audioNotification.audioFailedError1009": "Could not fetch STUN/TURN server information (error 1009)", + "app.audioNotification.audioFailedError1010": "Connection negotiation timeout (ICE error 1010)", + "app.audioNotification.audioFailedError1011": "Connection timeout (ICE error 1011)", + "app.audioNotification.audioFailedError1012": "Connection closed (ICE error 1012)", + "app.audioNotification.audioFailedMessage": "Your audio connection failed to connect", + "app.audioNotification.mediaFailedMessage": "getUserMicMedia failed as only secure origins are allowed", + "app.audioNotification.deviceChangeFailed": "Audio device change failed. Check if the chosen device is properly set up and available", + "app.audioNotification.closeLabel": "Close", + "app.audioNotificaion.reconnectingAsListenOnly": "Microphone has been locked for viewers, you are being connected as listen only", + "app.breakoutJoinConfirmation.title": "Join breakout room", + "app.breakoutJoinConfirmation.message": "Do you want to join", + "app.breakoutJoinConfirmation.confirmDesc": "Join you to the breakout room", + "app.breakoutJoinConfirmation.dismissLabel": "Cancel", + "app.breakoutJoinConfirmation.dismissDesc": "Closes and rejects joining the breakout room", + "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": "Change duration", + "app.breakout.dropdown.destroyAll": "End breakout rooms", + "app.breakout.dropdown.options": "Breakout Options", + "app.breakout.dropdown.manageUsers": "Manage users", + "app.calculatingBreakoutTimeRemaining": "Calculating remaining time ...", + "app.audioModal.ariaTitle": "Join audio modal", + "app.audioModal.microphoneLabel": "Microphone", + "app.audioModal.listenOnlyLabel": "Listen only", + "app.audioModal.microphoneDesc": "Joins audio conference with mic", + "app.audioModal.listenOnlyDesc": "Joins audio conference as listen only", + "app.audioModal.audioChoiceLabel": "How would you like to join the audio?", + "app.audioModal.iOSBrowser": "Audio/Video not supported", + "app.audioModal.iOSErrorDescription": "At this time audio and video are not supported on Chrome for iOS.", + "app.audioModal.iOSErrorRecommendation": "We recommend using Safari iOS.", + "app.audioModal.audioChoiceDesc": "Select how to join the audio in this meeting", + "app.audioModal.unsupportedBrowserLabel": "It looks like you're using a browser that is not fully supported. Please use either {0} or {1} for full support.", + "app.audioModal.closeLabel": "Close", + "app.audioModal.yes": "Yes", + "app.audioModal.no": "No", + "app.audioModal.yes.arialabel": "Echo is audible", + "app.audioModal.no.arialabel": "Echo is inaudible", + "app.audioModal.echoTestTitle": "This is a private echo test. Speak a few words. Did you hear audio?", + "app.audioModal.settingsTitle": "Change your audio settings", + "app.audioModal.helpTitle": "There was an issue with your media devices", + "app.audioModal.helpText": "Did you give permission for access to your microphone? Note that a dialog should appear when you try to join audio, asking for your media device permissions, please accept that in order to join the audio conference. If that is not the case, try changing your microphone permissions in your browser's settings.", + "app.audioModal.help.noSSL": "This page is unsecured. For microphone access to be allowed the page must be served over HTTPS. Please contact the server administrator.", + "app.audioModal.help.macNotAllowed": "It looks like your Mac System Preferences are blocking access to your microphone. Open System Preferences > Security & Privacy > Privacy > Microphone, and verify that the browser you're using is checked.", + "app.audioModal.audioDialTitle": "Join using your phone", + "app.audioDial.audioDialDescription": "Dial", + "app.audioDial.audioDialConfrenceText": "and enter the conference PIN number:", + "app.audioModal.autoplayBlockedDesc": "We need your permission to play audio.", + "app.audioModal.playAudio": "Play audio", + "app.audioModal.playAudio.arialabel": "Play audio", + "app.audioDial.tipIndicator": "Tip", + "app.audioDial.tipMessage": "Press the '0' key on your phone to mute/unmute yourself.", + "app.audioModal.connecting": "Establishing audio connection", + "app.audioManager.joinedAudio": "You have joined the audio conference", + "app.audioManager.joinedEcho": "You have joined the echo test", + "app.audioManager.leftAudio": "You have left the audio conference", + "app.audioManager.reconnectingAudio": "Attempting to reconnect audio", + "app.audioManager.genericError": "Error: An error has occurred, please try again", + "app.audioManager.connectionError": "Error: Connection error", + "app.audioManager.requestTimeout": "Error: There was a timeout in the request", + "app.audioManager.invalidTarget": "Error: Tried to request something to an invalid target", + "app.audioManager.mediaError": "Error: There was an issue getting your media devices", + "app.audio.joinAudio": "Join audio", + "app.audio.leaveAudio": "Leave audio", + "app.audio.changeAudioDevice": "Change audio device", + "app.audio.enterSessionLabel": "Enter session", + "app.audio.playSoundLabel": "Play sound", + "app.audio.stopAudioFeedback": "Stop audio feedback", + "app.audio.backLabel": "Back", + "app.audio.loading": "Loading", + "app.audio.microphones": "Microphones", + "app.audio.speakers": "Speakers", + "app.audio.noDeviceFound": "No device found", + "app.audio.audioSettings.titleLabel": "Choose your audio settings", + "app.audio.audioSettings.descriptionLabel": "Please note, a dialog will appear in your browser, requiring you to accept sharing your microphone.", + "app.audio.audioSettings.microphoneSourceLabel": "Microphone source", + "app.audio.audioSettings.speakerSourceLabel": "Speaker source", + "app.audio.audioSettings.testSpeakerLabel": "Test your speaker", + "app.audio.audioSettings.microphoneStreamLabel": "Your audio stream volume", + "app.audio.audioSettings.retryLabel": "Retry", + "app.audio.audioSettings.fallbackInputLabel": "Audio input {0}", + "app.audio.audioSettings.fallbackOutputLabel": "Audio output {0}", + "app.audio.audioSettings.defaultOutputDeviceLabel": "Default", + "app.audio.audioSettings.findingDevicesLabel": "Finding devices...", + "app.audio.listenOnly.backLabel": "Back", + "app.audio.listenOnly.closeLabel": "Close", + "app.audio.permissionsOverlay.title": "Allow access to your microphone", + "app.audio.permissionsOverlay.hint": "We need you to allow us to use your media devices in order to join you to the voice conference :)", + "app.audio.captions.button.start": "Start closed captions", + "app.audio.captions.button.stop": "Stop closed captions", + "app.audio.captions.button.language": "Language", + "app.audio.captions.button.transcription": "Transcription", + "app.audio.captions.button.transcriptionSettings": "Transcription settings", + "app.audio.captions.speech.title": "Automatic transcription", + "app.audio.captions.speech.disabled": "Disabled", + "app.audio.captions.speech.unsupported": "Your browser doesn't support speech recognition. Your audio won't be transcribed", + "app.audio.captions.select.de-DE": "German", + "app.audio.captions.select.en-US": "English", + "app.audio.captions.select.es-ES": "Spanish", + "app.audio.captions.select.fr-FR": "French", + "app.audio.captions.select.hi-ID": "Hindi", + "app.audio.captions.select.it-IT": "Italian", + "app.audio.captions.select.ja-JP": "Japanese", + "app.audio.captions.select.pt-BR": "Portuguese", + "app.audio.captions.select.ru-RU": "Russian", + "app.audio.captions.select.zh-CN": "Chinese", + "app.error.removed": "You have been removed from the conference", + "app.error.meeting.ended": "You have logged out of the conference", + "app.meeting.logout.duplicateUserEjectReason": "Duplicate user trying to join meeting", + "app.meeting.logout.permissionEjectReason": "Ejected due to permission violation", + "app.meeting.logout.ejectedFromMeeting": "You have been removed from the meeting", + "app.meeting.logout.validateTokenFailedEjectReason": "Failed to validate authorization token", + "app.meeting.logout.userInactivityEjectReason": "User inactive for too long", + "app.meeting.logout.maxParticipantsReached": "The maximum number of participants allowed for this meeting has been reached", + "app.meeting-ended.rating.legendLabel": "Feedback rating", + "app.meeting-ended.rating.starLabel": "Star", + "app.modal.close": "Close", + "app.modal.close.description": "Disregards changes and closes the modal", + "app.modal.confirm": "Done", + "app.modal.newTab": "(opens new tab)", + "app.modal.confirm.description": "Saves changes and closes the modal", + "app.modal.randomUser.noViewers.description": "No viewers available to randomly select from", + "app.modal.randomUser.selected.description": "You have been randomly selected", + "app.modal.randomUser.title": "Randomly selected user", + "app.modal.randomUser.who": "Who will be selected..?", + "app.modal.randomUser.alone": "There is only one viewer", + "app.modal.randomUser.reselect.label": "Select again", + "app.modal.randomUser.ariaLabel.title": "Randomly selected User Modal", + "app.dropdown.close": "Close", + "app.dropdown.list.item.activeLabel": "Active", + "app.error.400": "Bad Request", + "app.error.401": "Unauthorized", + "app.error.403": "You have been removed from the meeting", + "app.error.404": "Not found", + "app.error.408": "Authentication failed", + "app.error.409": "Conflict", + "app.error.410": "Meeting has ended", + "app.error.500": "Ops, something went wrong", + "app.error.503": "You have been disconnected", + "app.error.disconnected.rejoin": "You are able to refresh the page to rejoin.", + "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", + "app.error.fallback.presentation.description": "It has been logged. Please try reloading the page.", + "app.error.fallback.presentation.reloadButton": "Reload", + "app.guest.errorSeeConsole": "Error: more details in the console.", + "app.guest.noModeratorResponse": "No response from Moderator.", + "app.guest.noSessionToken": "No session Token received.", + "app.guest.windowTitle": "BigBlueButton - Guest Lobby", + "app.guest.missingToken": "Guest missing session token.", + "app.guest.missingSession": "Guest missing session.", + "app.guest.missingMeeting": "Meeting does not exist.", + "app.guest.meetingEnded": "Meeting ended.", + "app.guest.guestWait": "Please wait for a moderator to approve you joining the meeting.", + "app.guest.guestDeny": "Guest denied of joining the meeting.", + "app.guest.seatWait": "Guest waiting for a seat in the meeting.", + "app.guest.allow": "Guest approved and redirecting to meeting.", + "app.guest.firstPositionInWaitingQueue": "You are the first in line!", + "app.guest.positionInWaitingQueue": "Your current position in waiting queue: ", + "app.guest.guestInvalid": "Guest user is invalid", + "app.guest.meetingForciblyEnded": "You cannot join a meeting that has already been forcibly ended", + "app.userList.guest.waitingUsers": "Waiting Users", + "app.userList.guest.waitingUsersTitle": "User Management", + "app.userList.guest.optionTitle": "Review Pending Users", + "app.userList.guest.allowAllAuthenticated": "Allow all authenticated", + "app.userList.guest.allowAllGuests": "Allow all guests", + "app.userList.guest.allowEveryone": "Allow everyone", + "app.userList.guest.denyEveryone": "Deny everyone", + "app.userList.guest.pendingUsers": "{0} Pending Users", + "app.userList.guest.noPendingUsers": "Currently no pending users...", + "app.userList.guest.pendingGuestUsers": "{0} Pending Guest Users", + "app.userList.guest.pendingGuestAlert": "Has joined the session and is waiting for your approval.", + "app.userList.guest.rememberChoice": "Remember choice", + "app.userList.guest.emptyMessage": "There is currently no message", + "app.userList.guest.inputPlaceholder": "Message to the guests' lobby", + "app.userList.guest.privateInputPlaceholder": "Message to {0}", + "app.userList.guest.privateMessageLabel": "Message", + "app.userList.guest.acceptLabel": "Accept", + "app.userList.guest.denyLabel": "Deny", + "app.userList.guest.feedbackMessage": "Action applied: ", + "app.user-info.title": "Directory Lookup", + "app.toast.breakoutRoomEnded": "The breakout room ended. Please rejoin in the audio.", + "app.toast.chat.public": "New Public Chat message", + "app.toast.chat.private": "New Private Chat message", + "app.toast.chat.system": "System", + "app.toast.chat.poll": "Poll Results", + "app.toast.chat.pollClick": "Poll results were published. Click here to see.", + "app.toast.clearedEmoji.label": "Emoji status cleared", + "app.toast.setEmoji.label": "Emoji status set to {0}", + "app.toast.meetingMuteOn.label": "All users have been muted", + "app.toast.meetingMuteOnViewers.label": "All viewers have been muted", + "app.toast.meetingMuteOff.label": "Meeting mute turned off", + "app.toast.setEmoji.raiseHand": "You have raised your hand", + "app.toast.setEmoji.lowerHand": "Your hand has been lowered", + "app.toast.setEmoji.away": "You have set your status to away", + "app.toast.setEmoji.notAway": "You removed your away status", + "app.toast.promotedLabel": "You have been promoted to Moderator", + "app.toast.demotedLabel": "You have been demoted to Viewer", + "app.notification.recordingStart": "This session is now being recorded", + "app.notification.recordingStop": "This session is not being recorded", + "app.notification.recordingPaused": "This session is not being recorded anymore", + "app.notification.recordingAriaLabel": "Recorded time ", + "app.notification.userJoinPushAlert": "{0} joined the session", + "app.notification.userLeavePushAlert": "{0} left the session", + "app.submenu.notification.raiseHandLabel": "Raise hand", + "app.shortcut-help.title": "Keyboard shortcuts", + "app.shortcut-help.accessKeyNotAvailable": "Access keys not available", + "app.shortcut-help.comboLabel": "Combo", + "app.shortcut-help.alternativeLabel": "Alternative", + "app.shortcut-help.functionLabel": "Function", + "app.shortcut-help.closeLabel": "Close", + "app.shortcut-help.closeDesc": "Closes keyboard shortcuts modal", + "app.shortcut-help.openOptions": "Open Options", + "app.shortcut-help.toggleUserList": "Toggle UserList", + "app.shortcut-help.toggleMute": "Mute / Unmute", + "app.shortcut-help.togglePublicChat": "Toggle Public Chat (User list must be open)", + "app.shortcut-help.hidePrivateChat": "Hide private chat", + "app.shortcut-help.closePrivateChat": "Close private chat", + "app.shortcut-help.openActions": "Open actions menu", + "app.shortcut-help.raiseHand": "Toggle Raise Hand", + "app.shortcut-help.openDebugWindow": "Open debug window", + "app.shortcut-help.openStatus": "Open status menu", + "app.shortcut-help.togglePan": "Activate Pan tool (Presenter)", + "app.shortcut-help.toggleFullscreen": "Toggle Full-screen (Presenter)", + "app.shortcut-help.nextSlideDesc": "Next slide (Presenter)", + "app.shortcut-help.previousSlideDesc": "Previous slide (Presenter)", + "app.shortcut-help.togglePanKey": "Spacebar", + "app.shortcut-help.toggleFullscreenKey": "Enter", + "app.shortcut-help.nextSlideKey": "Right Arrow", + "app.shortcut-help.previousSlideKey": "Left Arrow", + "app.shortcut-help.select": "Select Tool", + "app.shortcut-help.pencil": "Pencil", + "app.shortcut-help.eraser": "Eraser", + "app.shortcut-help.rectangle": "Rectangle", + "app.shortcut-help.elipse": "Elipse", + "app.shortcut-help.triangle": "Triangle", + "app.shortcut-help.line": "Line", + "app.shortcut-help.arrow": "Arrow", + "app.shortcut-help.text": "Text Tool", + "app.shortcut-help.note": "Sticky Note", + "app.shortcut-help.general": "General", + "app.shortcut-help.presentation": "Presentation", + "app.shortcut-help.whiteboard": "Whiteboard", + "app.shortcut-help.zoomIn": "Zoom In", + "app.shortcut-help.zoomOut": "Zoom Out", + "app.shortcut-help.zoomFit": "Reset Zoom", + "app.shortcut-help.zoomSelect": "Zoom to Selection", + "app.shortcut-help.flipH": "Flip Horizontal", + "app.shortcut-help.flipV": "Flip Vertical", + "app.shortcut-help.lock": "Lock / Unlock", + "app.shortcut-help.moveToFront": "Move to Front", + "app.shortcut-help.moveToBack": "Move to Back", + "app.shortcut-help.moveForward": "Move Forward", + "app.shortcut-help.moveBackward": "Move Backward", + "app.shortcut-help.undo": "Undo", + "app.shortcut-help.redo": "Redo", + "app.shortcut-help.cut": "Cut", + "app.shortcut-help.copy": "Copy", + "app.shortcut-help.paste": "Paste", + "app.shortcut-help.selectAll": "Select All", + "app.shortcut-help.delete": "Delete", + "app.shortcut-help.duplicate": "Duplicate", + "app.lock-viewers.title": "Lock viewers", + "app.lock-viewers.description": "These options enable you to restrict viewers from using specific features.", + "app.lock-viewers.featuresLable": "Feature", + "app.lock-viewers.lockStatusLabel": "Status", + "app.lock-viewers.webcamLabel": "Share webcam", + "app.lock-viewers.otherViewersWebcamLabel": "See other viewers webcams", + "app.lock-viewers.microphoneLable": "Share microphone", + "app.lock-viewers.PublicChatLabel": "Send Public chat messages", + "app.lock-viewers.PrivateChatLable": "Send Private chat messages", + "app.lock-viewers.notesLabel": "Edit Shared Notes", + "app.lock-viewers.userListLabel": "See other viewers in the Users list", + "app.lock-viewers.ariaTitle": "Lock viewers settings modal", + "app.lock-viewers.button.apply": "Apply", + "app.lock-viewers.button.cancel": "Cancel", + "app.lock-viewers.locked": "Locked", + "app.lock-viewers.hideViewersCursor": "See other viewers cursors", + "app.lock-viewers.hideAnnotationsLabel": "See other viewers annotations", + "app.guest-policy.ariaTitle": "Guest policy settings modal", + "app.guest-policy.title": "Guest policy", + "app.guest-policy.description": "Change meeting guest policy setting", + "app.guest-policy.button.askModerator": "Ask moderator", + "app.guest-policy.button.alwaysAccept": "Always accept", + "app.guest-policy.button.alwaysDeny": "Always deny", + "app.guest-policy.policyBtnDesc": "Sets meeting guest policy", + "app.connection-status.ariaTitle": "Connection status modal", + "app.connection-status.title": "Connection status", + "app.connection-status.description": "View users' connection status", + "app.connection-status.empty": "There are currently no reported connection issues", + "app.connection-status.more": "more", + "app.connection-status.copy": "Copy Stats", + "app.connection-status.copied": "Copied!", + "app.connection-status.jitter": "Jitter", + "app.connection-status.label": "Connection status", + "app.connection-status.settings": "Adjusting Your Settings", + "app.connection-status.no": "No", + "app.connection-status.notification": "Loss in your connection was detected", + "app.connection-status.offline": "offline", + "app.connection-status.clientNotRespondingWarning": "Client not responding", + "app.connection-status.audioUploadRate": "Audio Upload Rate", + "app.connection-status.audioDownloadRate": "Audio Download Rate", + "app.connection-status.videoUploadRate": "Video Upload Rate", + "app.connection-status.videoDownloadRate": "Video Download Rate", + "app.connection-status.lostPackets": "Lost packets", + "app.connection-status.usingTurn": "Using TURN", + "app.connection-status.yes": "Yes", + "app.connection-status.connectionStats": "Connection Stats", + "app.connection-status.myLogs": "My Logs", + "app.connection-status.sessionLogs": "Session Logs", + "app.connection-status.next": "Next page", + "app.connection-status.prev": "Previous page", + "app.learning-dashboard.label": "Learning Analytics Dashboard", + "app.learning-dashboard.description": "Dashboard with users activities", + "app.learning-dashboard.clickHereToOpen": "Open Learning Analytics Dashboard", + "app.recording.startTitle": "Start recording", + "app.recording.stopTitle": "Pause recording", + "app.recording.resumeTitle": "Resume recording", + "app.recording.startDescription": "You can select the record button again later to pause the recording.", + "app.recording.stopDescription": "Are you sure you want to pause the recording? You can resume by selecting the record button again.", + "app.recording.notify.title": "Recording has started", + "app.recording.notify.description": "A recording will be available based on the remainder of this session", + "app.recording.notify.continue": "Continue", + "app.recording.notify.leave": "Leave session", + "app.recording.notify.continueLabel": "Accept recording and continue", + "app.recording.notify.leaveLabel": "Do not accept recording and leave meeting", + "app.videoPreview.cameraLabel": "Camera", + "app.videoPreview.profileLabel": "Quality", + "app.videoPreview.quality.low": "Low", + "app.videoPreview.quality.medium": "Medium", + "app.videoPreview.quality.high": "High", + "app.videoPreview.quality.hd": "High definition", + "app.videoPreview.cancelLabel": "Cancel", + "app.videoPreview.closeLabel": "Close", + "app.videoPreview.findingWebcamsLabel": "Finding webcams", + "app.videoPreview.startSharingLabel": "Start sharing", + "app.videoPreview.stopSharingLabel": "Stop sharing", + "app.videoPreview.stopSharingAllLabel": "Stop all", + "app.videoPreview.sharedCameraLabel": "This camera is already being shared", + "app.videoPreview.webcamOptionLabel": "Choose webcam", + "app.videoPreview.webcamPreviewLabel": "Webcam preview", + "app.videoPreview.webcamSettingsTitle": "Webcam settings", + "app.videoPreview.webcamEffectsTitle": "Webcam visual effects", + "app.videoPreview.cameraAsContentSettingsTitle": "Present Camera", + "app.videoPreview.webcamVirtualBackgroundLabel": "Virtual background settings", + "app.videoPreview.webcamVirtualBackgroundDisabledLabel": "This device does not support virtual backgrounds", + "app.videoPreview.webcamNotFoundLabel": "Webcam not found", + "app.videoPreview.profileNotFoundLabel": "No supported camera profile", + "app.videoPreview.brightness": "Brightness", + "app.videoPreview.wholeImageBrightnessLabel": "Whole image", + "app.videoPreview.wholeImageBrightnessDesc": "Applies brightness to stream and background image", + "app.videoPreview.sliderDesc": "Increase or decrease levels of brightness", + "app.video.joinVideo": "Share webcam", + "app.video.connecting": "Webcam sharing is starting ...", + "app.video.leaveVideo": "Stop sharing webcam", + "app.video.videoSettings": "Video settings", + "app.video.visualEffects": "Visual effects", + "app.video.advancedVideo": "Open advanced settings", + "app.video.iceCandidateError": "Error on adding ICE candidate", + "app.video.iceConnectionStateError": "Connection failure (ICE error 1107)", + "app.video.permissionError": "Error on sharing webcam. Please check permissions", + "app.video.sharingError": "Error on sharing webcam", + "app.video.abortError": "An unknown problem occurred which prevented your camera from being used", + "app.video.overconstrainedError": "Your camera does not support this quality profile", + "app.video.securityError": "Your browser has disabled camera usage. Try a different browser", + "app.video.typeError": "Invalid camera quality profile. Contact your administrator", + "app.video.notFoundError": "No webcams found. Please make sure there's one connected", + "app.video.notAllowed": "Permission to access webcams needs to be granted", + "app.video.notSupportedError": "Browser is not supported. Try again using a different browser or device", + "app.video.notReadableError": "Could not get webcam video. Please make sure another program is not using the webcam ", + "app.video.timeoutError": "Browser did not respond in time.", + "app.video.genericError": "An unknown error has occurred with the device ({0})", + "app.video.inactiveError": "Your webcam stopped unexpectedly. Please review your browser's permissions", + "app.video.mediaTimedOutError": "Your webcam stream has been interrupted. Try sharing it again", + "app.video.mediaFlowTimeout1020": "Media could not reach the server (error 1020)", + "app.video.suggestWebcamLock": "Enforce lock setting to viewers webcams?", + "app.video.suggestWebcamLockReason": "(this will improve the stability of the meeting)", + "app.video.enable": "Enable", + "app.video.cancel": "Cancel", + "app.video.swapCam": "Swap", + "app.video.swapCamDesc": "swap the direction of webcams", + "app.video.videoLocked": "Webcam sharing locked", + "app.video.videoButtonDesc": "Share webcam", + "app.video.videoMenu": "Video menu", + "app.video.videoMenuDisabled": "Video menu Webcam is disabled in settings", + "app.video.videoMenuDesc": "Open video menu dropdown", + "app.video.pagination.prevPage": "See previous videos", + "app.video.pagination.nextPage": "See next videos", + "app.video.clientDisconnected": "Webcam cannot be shared due to connection issues", + "app.video.virtualBackground.none": "None", + "app.video.virtualBackground.blur": "Blur", + "app.video.virtualBackground.home": "Home", + "app.video.virtualBackground.board": "Board", + "app.video.virtualBackground.coffeeshop": "Coffeeshop", + "app.video.virtualBackground.background": "Background", + "app.video.virtualBackground.backgroundWithIndex": "Background {0}", + "app.video.virtualBackground.custom": "Upload from your computer", + "app.video.virtualBackground.remove": "Remove added image", + "app.video.virtualBackground.genericError": "Failed to apply camera effect. Try again.", + "app.video.virtualBackground.camBgAriaDesc": "Sets webcam virtual background to {0}", + "app.video.virtualBackground.maximumFileSizeExceeded": "Maximum file size exceeded. ({0}MB)", + "app.video.virtualBackground.typeNotAllowed": "File type not allowed.", + "app.video.virtualBackground.errorOnRead": "Something went wrong when reading the file.", + "app.video.virtualBackground.uploaded": "Uploaded", + "app.video.virtualBackground.uploading": "Uploading...", + "app.video.virtualBackground.button.customDesc": "Adds a new virtual background image", + "app.video.camCapReached": "You cannot share more cameras", + "app.video.meetingCamCapReached": "Meeting reached it's simultaneous cameras limit", + "app.video.dropZoneLabel": "Drop here", + "app.fullscreenButton.label": "Make {0} fullscreen", + "app.fullscreenUndoButton.label": "Undo {0} fullscreen", + "app.switchButton.expandLabel": "Expand screenshare video", + "app.switchButton.shrinkLabel": "Shrink screenshare video", + "app.sfu.mediaServerConnectionError2000": "Unable to connect to media server (error 2000)", + "app.sfu.mediaServerOffline2001": "Media server is offline. Please try again later (error 2001)", + "app.sfu.mediaServerNoResources2002": "Media server has no available resources (error 2002)", + "app.sfu.mediaServerRequestTimeout2003": "Media server requests are timing out (error 2003)", + "app.sfu.serverIceGatheringFailed2021": "Media server cannot gather connection candidates (ICE error 2021)", + "app.sfu.serverIceGatheringFailed2022": "Media server connection failed (ICE error 2022)", + "app.sfu.mediaGenericError2200": "Media server failed to process request (error 2200)", + "app.sfu.invalidSdp2202": "Client generated an invalid media request (SDP error 2202)", + "app.sfu.noAvailableCodec2203": "Server could not find an appropriate codec (error 2203)", + "app.meeting.endNotification.ok.label": "OK", + "app.whiteboard.annotations.poll": "Poll results were published", + "app.whiteboard.annotations.pollResult": "Poll Result", + "app.whiteboard.annotations.noResponses": "No responses", + "app.whiteboard.annotations.notAllowed": "You are not allowed to make this change", + "app.whiteboard.annotations.numberExceeded": "The number of annotations exceeded the limit ({0})", + "app.whiteboard.toolbar.tools": "Tools", + "app.whiteboard.toolbar.tools.hand": "Pan", + "app.whiteboard.toolbar.tools.pencil": "Pencil", + "app.whiteboard.toolbar.tools.rectangle": "Rectangle", + "app.whiteboard.toolbar.tools.triangle": "Triangle", + "app.whiteboard.toolbar.tools.ellipse": "Ellipse", + "app.whiteboard.toolbar.tools.line": "Line", + "app.whiteboard.toolbar.tools.text": "Text", + "app.whiteboard.toolbar.thickness": "Drawing thickness", + "app.whiteboard.toolbar.thicknessDisabled": "Drawing thickness is disabled", + "app.whiteboard.toolbar.color": "Colors", + "app.whiteboard.toolbar.colorDisabled": "Colors is disabled", + "app.whiteboard.toolbar.color.black": "Black", + "app.whiteboard.toolbar.color.white": "White", + "app.whiteboard.toolbar.color.red": "Red", + "app.whiteboard.toolbar.color.orange": "Orange", + "app.whiteboard.toolbar.color.eletricLime": "Electric lime", + "app.whiteboard.toolbar.color.lime": "Lime", + "app.whiteboard.toolbar.color.cyan": "Cyan", + "app.whiteboard.toolbar.color.dodgerBlue": "Dodger blue", + "app.whiteboard.toolbar.color.blue": "Blue", + "app.whiteboard.toolbar.color.violet": "Violet", + "app.whiteboard.toolbar.color.magenta": "Magenta", + "app.whiteboard.toolbar.color.silver": "Silver", + "app.whiteboard.toolbar.undo": "Undo annotation", + "app.whiteboard.toolbar.clear": "Clear all annotations", + "app.whiteboard.toolbar.clearConfirmation": "Are you sure you want to clear all annotations?", + "app.whiteboard.toolbar.multiUserOn": "Turn multi-user whiteboard on", + "app.whiteboard.toolbar.multiUserOff": "Turn multi-user whiteboard off", + "app.whiteboard.toolbar.palmRejectionOn": "Turn palm rejection on", + "app.whiteboard.toolbar.palmRejectionOff": "Turn palm rejection off", + "app.whiteboard.toolbar.fontSize": "Font size list", + "app.whiteboard.toolbarAriaLabel": "Presentation tools", + "app.feedback.title": "You have logged out of the conference", + "app.feedback.subtitle": "We'd love to hear about your experience with BigBlueButton (optional)", + "app.feedback.textarea": "How can we make BigBlueButton better?", + "app.feedback.sendFeedback": "Send Feedback", + "app.feedback.sendFeedbackDesc": "Send a feedback and leave the meeting", + "app.videoDock.webcamMirrorLabel": "Mirror", + "app.videoDock.webcamMirrorDesc": "Mirror the selected webcam", + "app.videoDock.webcamFocusLabel": "Focus", + "app.videoDock.webcamFocusDesc": "Focus the selected webcam", + "app.videoDock.webcamUnfocusLabel": "Unfocus", + "app.videoDock.webcamUnfocusDesc": "Unfocus the selected webcam", + "app.videoDock.webcamDisableLabel": "Disable self-view", + "app.videoDock.webcamEnableLabel": "Enable self-view", + "app.videoDock.webcamDisableDesc": "Self-view disabled", + "app.videoDock.webcamPinLabel": "Pin", + "app.videoDock.webcamPinDesc": "Pin the selected webcam", + "app.videoDock.webcamFullscreenLabel": "Fullscreen webcam", + "app.videoDock.webcamSqueezedButtonLabel": "Webcam options", + "app.videoDock.webcamUnpinLabel": "Unpin", + "app.videoDock.webcamUnpinLabelDisabled": "Only moderators can unpin users", + "app.videoDock.webcamUnpinDesc": "Unpin the selected webcam", + "app.videoDock.autoplayBlockedDesc": "We need your permission to show you other users' webcams.", + "app.videoDock.autoplayAllowLabel": "View webcams", + "app.createBreakoutRoom.title": "Breakout Rooms", + "app.createBreakoutRoom.ariaTitle": "Hide Breakout Rooms", + "app.createBreakoutRoom.breakoutRoomLabel": "Breakout Rooms {0}", + "app.createBreakoutRoom.askToJoin": "Ask to join", + "app.createBreakoutRoom.generatingURL": "Generating URL", + "app.createBreakoutRoom.generatingURLMessage": "We are generating a join URL for the selected breakout room. It may take a few seconds...", + "app.createBreakoutRoom.duration": "Duration {0}", + "app.createBreakoutRoom.room": "Room {0}", + "app.createBreakoutRoom.notAssigned": "Not assigned ({0})", + "app.createBreakoutRoom.join": "Join room", + "app.createBreakoutRoom.joinAudio": "Join audio", + "app.createBreakoutRoom.returnAudio": "Return audio", + "app.createBreakoutRoom.alreadyConnected": "Already in room", + "app.createBreakoutRoom.confirm": "Create", + "app.createBreakoutRoom.record": "Record", + "app.createBreakoutRoom.numberOfRooms": "Number of rooms", + "app.createBreakoutRoom.durationInMinutes": "Duration (minutes)", + "app.createBreakoutRoom.randomlyAssign": "Randomly assign", + "app.createBreakoutRoom.randomlyAssignDesc": "Assigns users randomly to breakout rooms", + "app.createBreakoutRoom.resetAssignments": "Reset assignments", + "app.createBreakoutRoom.resetAssignmentsDesc": "Reset all user room assignments", + "app.createBreakoutRoom.endAllBreakouts": "End all breakout rooms", + "app.createBreakoutRoom.chatTitleMsgAllRooms": "all rooms", + "app.createBreakoutRoom.msgToBreakoutsSent": "Message was sent to {0} breakout rooms", + "app.createBreakoutRoom.roomName": "{0} (Room - {1})", + "app.createBreakoutRoom.doneLabel": "Done", + "app.createBreakoutRoom.nextLabel": "Next", + "app.createBreakoutRoom.minusRoomTime": "Decrease breakout room time to", + "app.createBreakoutRoom.addRoomTime": "Increase breakout room time to", + "app.createBreakoutRoom.addParticipantLabel": "+ Add participant", + "app.createBreakoutRoom.freeJoin": "Allow users to choose a breakout room to join", + "app.createBreakoutRoom.captureNotes": "Capture shared notes when breakout rooms end", + "app.createBreakoutRoom.sendInvitationToMods": "Send invitation to assigned moderators", + "app.createBreakoutRoom.captureSlides": "Capture whiteboard when breakout rooms end", + "app.createBreakoutRoom.leastOneWarnBreakout": "You must place at least one user in a breakout room.", + "app.createBreakoutRoom.minimumDurationWarnBreakout": "Minimum duration for a breakout room is {0} minutes.", + "app.createBreakoutRoom.modalDesc": "Tip: You can drag-and-drop a user's name to assign them to a specific breakout room.", + "app.createBreakoutRoom.roomTime": "{0} minutes", + "app.createBreakoutRoom.numberOfRoomsError": "The number of rooms is invalid.", + "app.createBreakoutRoom.duplicatedRoomNameError": "Room name can't be duplicated.", + "app.createBreakoutRoom.emptyRoomNameError": "Room name can't be empty.", + "app.createBreakoutRoom.setTimeInMinutes": "Set duration to (minutes)", + "app.createBreakoutRoom.setTimeLabel": "Apply", + "app.createBreakoutRoom.setTimeCancel": "Cancel", + "app.createBreakoutRoom.setTimeHigherThanMeetingTimeError": "The breakout rooms duration can't exceed the meeting remaining time.", + "app.createBreakoutRoom.roomNameInputDesc": "Updates breakout room name", + "app.createBreakoutRoom.movedUserLabel": "Moved {0} to room {1}", + "app.updateBreakoutRoom.modalDesc": "To update or invite a user, simply drag them into the desired room.", + "app.updateBreakoutRoom.cancelLabel": "Cancel", + "app.updateBreakoutRoom.title": "Update Breakout Rooms", + "app.updateBreakoutRoom.confirm": "Apply", + "app.updateBreakoutRoom.userChangeRoomNotification": "You were moved to room {0}.", + "app.smartMediaShare.externalVideo": "External video(s)", + "app.update.resetRoom": "Reset user room", + "app.externalVideo.start": "Share a new video", + "app.externalVideo.title": "Share an external video", + "app.externalVideo.input": "External Video URL", + "app.externalVideo.urlInput": "Add Video URL", + "app.externalVideo.urlError": "This video URL isn't supported", + "app.externalVideo.close": "Close", + "app.externalVideo.autoPlayWarning": "Play the video to enable media synchronization", + "app.externalVideo.refreshLabel": "Refresh Video Player", + "app.externalVideo.fullscreenLabel": "Video Player", + "app.externalVideo.noteLabel": "Note: Shared external videos will not appear in the recording. YouTube, Vimeo, Instructure Media, Twitch, Dailymotion and media file URLs (e.g. https://example.com/xy.mp4) are supported.", + "app.externalVideo.subtitlesOn": "Turn off", + "app.externalVideo.subtitlesOff": "Turn on (if available)", + "app.actionsBar.actionsDropdown.shareExternalVideo": "Share an external video", + "app.actionsBar.actionsDropdown.stopShareExternalVideo": "Stop sharing external video", + "app.legacy.unsupportedBrowser": "It looks like you're using a browser that is not supported. Please use either {0} or {1} for full support.", + "app.legacy.upgradeBrowser": "It looks like you're using an older version of a supported browser. Please upgrade your browser for full support.", + "app.legacy.criosBrowser": "On iOS please use Safari for full support.", + "app.debugWindow.windowTitle": "Debug", + "app.debugWindow.form.userAgentLabel": "User Agent", + "app.debugWindow.form.button.copy": "Copy", + "app.debugWindow.form.enableAutoarrangeLayoutLabel": "Enable Auto Arrange Layout", + "app.debugWindow.form.enableAutoarrangeLayoutDescription": "(it will be disabled if you drag or resize the webcams area)", + "app.debugWindow.form.chatLoggerLabel": "Test Chat Logger Levels", + "app.debugWindow.form.button.apply": "Apply", + "app.layout.modal.title": "Layouts", + "app.layout.modal.confirm": "Confirm", + "app.layout.modal.cancel": "Cancel", + "app.layout.modal.layoutLabel": "Select your layout", + "app.layout.modal.keepPushingLayoutLabel": "Push layout to all", + "app.layout.modal.pushLayoutLabel": "Push to everyone", + "app.layout.modal.layoutToastLabel": "Layout settings changed", + "app.layout.modal.layoutSingular": "Layout", + "app.layout.modal.layoutBtnDesc": "Sets layout as selected option", + "app.layout.style.custom": "Custom", + "app.layout.style.smart": "Smart layout", + "app.layout.style.presentationFocus": "Focus on presentation", + "app.layout.style.videoFocus": "Focus on video", + "app.layout.style.customPush": "Custom (push layout to all)", + "app.layout.style.smartPush": "Smart layout (push layout to all)", + "app.layout.style.presentationFocusPush": "Focus on presentation (push layout to all)", + "app.layout.style.videoFocusPush": "Focus on video (push layout to all)", + "playback.button.about.aria": "About", + "playback.button.clear.aria": "Clear search", + "playback.button.close.aria": "Close modal", + "playback.button.fullscreen.aria": "Fullscreen content", + "playback.button.restore.aria": "Restore content", + "playback.button.search.aria": "Search", + "playback.button.section.aria": "Side section", + "playback.button.swap.aria": "Swap content", + "playback.button.theme.aria": "Toggle theme", + "playback.error.wrapper.aria": "Error area", + "playback.loader.wrapper.aria": "Loader area", + "playback.player.wrapper.aria": "Player area", + "playback.player.about.modal.shortcuts.title": "Shortcuts", + "playback.player.about.modal.shortcuts.alt": "Alt", + "playback.player.about.modal.shortcuts.shift": "Shift", + "playback.player.about.modal.shortcuts.fullscreen": "Toggle fullscreen", + "playback.player.about.modal.shortcuts.play": "Play/Pause", + "playback.player.about.modal.shortcuts.section": "Toggle side section", + "playback.player.about.modal.shortcuts.seek.backward": "Seek backwards", + "playback.player.about.modal.shortcuts.seek.forward": "Seek forwards", + "playback.player.about.modal.shortcuts.skip.next": "Next slide", + "playback.player.about.modal.shortcuts.skip.previous": "Previous Slide", + "playback.player.about.modal.shortcuts.swap": "Swap content", + "playback.player.chat.message.poll.name": "Poll result", + "playback.player.chat.message.poll.question": "Question", + "playback.player.chat.message.poll.options": "Options", + "playback.player.chat.message.poll.option.yes": "Yes", + "playback.player.chat.message.poll.option.no": "No", + "playback.player.chat.message.poll.option.abstention": "Abstention", + "playback.player.chat.message.poll.option.true": "True", + "playback.player.chat.message.poll.option.false": "False", + "playback.player.chat.message.video.name": "External video", + "playback.player.chat.wrapper.aria": "Chat area", + "playback.player.notes.wrapper.aria": "Notes area", + "playback.player.presentation.wrapper.aria": "Presentation area", + "playback.player.screenshare.wrapper.aria": "Screenshare area", + "playback.player.search.modal.title": "Search", + "playback.player.search.modal.subtitle": "Find presentation slides content", + "playback.player.thumbnails.wrapper.aria": "Thumbnails area", + "playback.player.webcams.wrapper.aria": "Webcams area", + "app.learningDashboard.dashboardTitle": "Learning Analytics Dashboard", + "app.learningDashboard.bigbluebuttonTitle": "BigBlueButton", + "app.learningDashboard.downloadSessionDataLabel": "Download Session Data", + "app.learningDashboard.lastUpdatedLabel": "Last updated at", + "app.learningDashboard.sessionDataDownloadedLabel": "Downloaded!", + "app.learningDashboard.shareButton": "Share with others", + "app.learningDashboard.shareLinkCopied": "Link successfully copied!", + "app.learningDashboard.user": "User", + "app.learningDashboard.indicators.meetingStatusEnded": "Ended", + "app.learningDashboard.indicators.meetingStatusActive": "Active", + "app.learningDashboard.indicators.usersOnline": "Active Users", + "app.learningDashboard.indicators.usersTotal": "Total Number Of Users", + "app.learningDashboard.indicators.polls": "Polls", + "app.learningDashboard.indicators.timeline": "Timeline", + "app.learningDashboard.indicators.activityScore": "Activity Score", + "app.learningDashboard.indicators.duration": "Duration", + "app.learningDashboard.userDetails.startTime": "Start Time", + "app.learningDashboard.userDetails.endTime": "End Time", + "app.learningDashboard.userDetails.joined": "Joined", + "app.learningDashboard.userDetails.category": "Category", + "app.learningDashboard.userDetails.average": "Average", + "app.learningDashboard.userDetails.activityPoints": "Activity Points", + "app.learningDashboard.userDetails.poll": "Poll", + "app.learningDashboard.userDetails.response": "Response", + "app.learningDashboard.userDetails.mostCommonAnswer": "Most Common Answer", + "app.learningDashboard.userDetails.anonymousAnswer": "Anonymous Poll", + "app.learningDashboard.userDetails.talkTime": "Talk Time", + "app.learningDashboard.userDetails.messages": "Messages", + "app.learningDashboard.userDetails.emojis": "Emojis", + "app.learningDashboard.userDetails.raiseHands": "Raise Hands", + "app.learningDashboard.userDetails.pollVotes": "Poll Votes", + "app.learningDashboard.userDetails.onlineIndicator": "{0} online time", + "app.learningDashboard.usersTable.title": "Overview", + "app.learningDashboard.usersTable.colOnline": "Online time", + "app.learningDashboard.usersTable.colTalk": "Talk time", + "app.learningDashboard.usersTable.colWebcam": "Webcam Time", + "app.learningDashboard.usersTable.colMessages": "Messages", + "app.learningDashboard.usersTable.colEmojis": "Emojis", + "app.learningDashboard.usersTable.colRaiseHands": "Raise Hands", + "app.learningDashboard.usersTable.colActivityScore": "Activity Score", + "app.learningDashboard.usersTable.colStatus": "Status", + "app.learningDashboard.usersTable.userStatusOnline": "Online", + "app.learningDashboard.usersTable.userStatusOffline": "Offline", + "app.learningDashboard.usersTable.noUsers": "No users yet", + "app.learningDashboard.usersTable.name": "Name", + "app.learningDashboard.usersTable.moderator": "Moderator", + "app.learningDashboard.usersTable.pollVotes": "Poll Votes", + "app.learningDashboard.usersTable.join": "Join", + "app.learningDashboard.usersTable.left": "Left", + "app.learningDashboard.usersTable.notAvailable": "N/A", + "app.learningDashboard.pollsTable.title": "Polls", + "app.learningDashboard.pollsTable.anonymousAnswer": "Anonymous Poll (answers in the last row)", + "app.learningDashboard.pollsTable.anonymousRowName": "Anonymous", + "app.learningDashboard.pollsTable.noPollsCreatedHeading": "No polls have been created", + "app.learningDashboard.pollsTable.noPollsCreatedMessage": "Once a poll has been sent to users, their results will appear in this list.", + "app.learningDashboard.pollsTable.answerTotal": "Total", + "app.learningDashboard.pollsTable.userLabel": "User", + "app.learningDashboard.statusTimelineTable.title": "Timeline", + "app.learningDashboard.statusTimelineTable.thumbnail": "Presentation thumbnail", + "app.learningDashboard.statusTimelineTable.presentation": "Presentation", + "app.learningDashboard.statusTimelineTable.pageNumber": "Page", + "app.learningDashboard.statusTimelineTable.setAt": "Set at", + "app.learningDashboard.errors.invalidToken": "Invalid session token", + "app.learningDashboard.errors.dataUnavailable": "Data is no longer available", + "mobileApp.portals.list.empty.addFirstPortal.label": "Add your first portal using the button above,", + "mobileApp.portals.list.empty.orUseOurDemoServer.label": "or use our demo server.", + "mobileApp.portals.list.add.button.label": "Add portal", + "mobileApp.portals.fields.name.label": "Portal Name", + "mobileApp.portals.fields.name.placeholder": "BigBlueButton demo", + "mobileApp.portals.fields.url.label": "Server URL", + "mobileApp.portals.addPortalPopup.confirm.button.label": "Save", + "mobileApp.portals.drawerNavigation.button.label": "Portals", + "mobileApp.portals.addPortalPopup.validation.emptyFields": "Required Fields", + "mobileApp.portals.addPortalPopup.validation.portalNameAlreadyExists": "Name already in use", + "mobileApp.portals.addPortalPopup.validation.urlInvalid": "Error trying to load the page - check URL and network connection" }