diff --git a/bigbluebutton-html5/imports/api/shapes/server/modifiers/addShape.js b/bigbluebutton-html5/imports/api/shapes/server/modifiers/addShape.js index 231d53c9c79ec2d362c5a2fec2dae85b12c9ac9c..1e15472fe41a028cc74f51b00a52c55b92fc5ce0 100644 --- a/bigbluebutton-html5/imports/api/shapes/server/modifiers/addShape.js +++ b/bigbluebutton-html5/imports/api/shapes/server/modifiers/addShape.js @@ -43,7 +43,7 @@ export default function addShape(meetingId, whiteboardId, shape) { 'shape.shape.id': shape.shape.id, 'shape.shape.y': shape.shape.y, 'shape.shape.calcedFontSize': shape.shape.calcedFontSize, - 'shape.shape.text': shape.shape.text, + 'shape.shape.text': shape.shape.text.replace(/[\r]/g, '\n'), }); break; diff --git a/bigbluebutton-html5/imports/startup/client/intl.js b/bigbluebutton-html5/imports/startup/client/intl.js index 8c0ab25d02ededf59f6bce7f41d44354ba4a1f4d..6b14147766b74400eaca1bed868d8f7182d06928 100644 --- a/bigbluebutton-html5/imports/startup/client/intl.js +++ b/bigbluebutton-html5/imports/startup/client/intl.js @@ -15,8 +15,9 @@ class IntlStartup extends Component { this.state = { messages: {}, + appLocale : this.props.locale, }; - + this.fetchLocalizedMessages = this.fetchLocalizedMessages.bind(this); } @@ -27,7 +28,14 @@ class IntlStartup extends Component { baseControls.updateLoadingState(true); fetch(url) - .then(response => response.json()) + .then(response => { + if (response.ok) { + return response.json(); + } else { + this.setState({appLocale: 'en'}); + return response.json(); + } + }) .then(messages => { this.setState({ messages }, () => { baseControls.updateLoadingState(false); @@ -40,18 +48,19 @@ class IntlStartup extends Component { } componentWillMount() { - this.fetchLocalizedMessages(this.props.locale); + this.fetchLocalizedMessages(this.state.appLocale); } componentWillUpdate(nextProps, nextState) { if (this.props.locale !== nextProps.locale) { + this.setState({appLocale: nextProps.locale}); this.fetchLocalizedMessages(nextProps.locale); } } render() { return ( - <IntlProvider locale={this.props.locale} messages={this.state.messages}> + <IntlProvider locale={this.state.appLocale} messages={this.state.messages}> {this.props.children} </IntlProvider> ); diff --git a/bigbluebutton-html5/imports/startup/server/index.js b/bigbluebutton-html5/imports/startup/server/index.js index a8371245e4054f5314c6ead9f7e28d39bfbf0cae..5c64b14063bcfc8ac9c8280cb1ee87526ac40dea 100755 --- a/bigbluebutton-html5/imports/startup/server/index.js +++ b/bigbluebutton-html5/imports/startup/server/index.js @@ -24,9 +24,8 @@ WebApp.connectHandlers.use('/locale', (req, res) => { let defaultLocale = APP_CONFIG.defaultLocale; let localeRegion = req.query.locale.split('-'); let messages = {}; - let locales = [defaultLocale, localeRegion[0]]; - + let statusCode = 200; if (localeRegion.length > 1) { locales.push(`${localeRegion[0]}_${localeRegion[1].toUpperCase()}`); } @@ -36,14 +35,15 @@ WebApp.connectHandlers.use('/locale', (req, res) => { const data = Assets.getText(`locales/${locale}.json`); messages = Object.assign(messages, JSON.parse(data)); } catch (e) { - // console.error(e); - // We dont really care about those errors since they will be a parse error - // or a file not found which is ok + //Variant Also Negotiates Status-Code, to alert the client that we + //do not support the following lang. + //https://en.wikipedia.org/wiki/Content_negotiation + statusCode = 506; } }); res.setHeader('Content-Type', 'application/json'); - res.writeHead(200); + res.writeHead(statusCode); res.end(JSON.stringify(messages)); }); diff --git a/bigbluebutton-html5/imports/startup/server/userPermissions.js b/bigbluebutton-html5/imports/startup/server/userPermissions.js index 4fea3ba0ea46af05952924788959e010afec6cb2..750e536580db121b8f8a0c385275e024f87438a3 100755 --- a/bigbluebutton-html5/imports/startup/server/userPermissions.js +++ b/bigbluebutton-html5/imports/startup/server/userPermissions.js @@ -129,12 +129,23 @@ export function isAllowedTo(action, credentials) { }); const allowedToInitiateRequest = - null != user && - authToken === user.authToken && + user && + user.authToken === authToken && user.validated && - user.user.connection_status === 'online' && - 'HTML5' === user.clientType && - null != user.user; + user.clientType === 'HTML5' && + user.user && + user.user.connection_status === 'online'; + + const listOfSafeActions = [ + 'logoutSelf', + ]; + + const requestIsSafe = listOfSafeActions.includes(action); + + if (requestIsSafe) { + logger.info(`permissions: requestIsSafe for ${action} by userId=${userId} allowed`); + return true; + } if (allowedToInitiateRequest) { let result = false; diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx index e880898ebfb1eef070c58855ce00a0fdd4b03a29..e0d3cf3b83fc660e42eec98028babb0e4211ee00 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx @@ -18,6 +18,7 @@ const propTypes = { class EmojiMenu extends Component { constructor(props) { super(props); + } render() { @@ -28,8 +29,8 @@ class EmojiMenu extends Component { } = this.props; return ( - <Dropdown ref="dropdown"> - <DropdownTrigger> + <Dropdown autoFocus={true}> + <DropdownTrigger placeInTabOrder={true}> <Button role="button" label={intl.formatMessage(intlMessages.statusTriggerLabel)} @@ -46,7 +47,7 @@ class EmojiMenu extends Component { // even after the DropdownTrigger inject an onClick handler onClick={() => null}> <div id="currentStatus" hidden> - {intl.formatMessage(intlMessages.currentStatusDesc, { status: userEmojiStatus}) } + {intl.formatMessage(intlMessages.currentStatusDesc, { 0: userEmojiStatus}) } </div> </Button> </DropdownTrigger> @@ -57,54 +58,63 @@ class EmojiMenu extends Component { label={intl.formatMessage(intlMessages.raiseLabel)} description={intl.formatMessage(intlMessages.raiseDesc)} onClick={() => actions.setEmojiHandler('raiseHand')} + tabIndex={-1} /> <DropdownListItem icon="happy" label={intl.formatMessage(intlMessages.happyLabel)} description={intl.formatMessage(intlMessages.happyDesc)} onClick={() => actions.setEmojiHandler('happy')} + tabIndex={-1} /> <DropdownListItem icon="undecided" label={intl.formatMessage(intlMessages.undecidedLabel)} description={intl.formatMessage(intlMessages.undecidedDesc)} onClick={() => actions.setEmojiHandler('neutral')} + tabIndex={-1} /> <DropdownListItem icon="sad" label={intl.formatMessage(intlMessages.sadLabel)} description={intl.formatMessage(intlMessages.sadDesc)} onClick={() => actions.setEmojiHandler('sad')} + tabIndex={-1} /> <DropdownListItem icon="confused" label={intl.formatMessage(intlMessages.confusedLabel)} description={intl.formatMessage(intlMessages.confusedDesc)} onClick={() => actions.setEmojiHandler('confused')} + tabIndex={-1} /> <DropdownListItem icon="time" label={intl.formatMessage(intlMessages.awayLabel)} description={intl.formatMessage(intlMessages.awayDesc)} onClick={() => actions.setEmojiHandler('away')} + tabIndex={-1} /> <DropdownListItem icon="thumbs_up" label={intl.formatMessage(intlMessages.thumbsupLabel)} description={intl.formatMessage(intlMessages.thumbsupDesc)} onClick={() => actions.setEmojiHandler('thumbsUp')} + tabIndex={-1} /> <DropdownListItem icon="thumbs_down" label={intl.formatMessage(intlMessages.thumbsdownLabel)} description={intl.formatMessage(intlMessages.thumbsdownDesc)} onClick={() => actions.setEmojiHandler('thumbsDown')} + tabIndex={-1} /> <DropdownListItem icon="applause" label={intl.formatMessage(intlMessages.applauseLabel)} description={intl.formatMessage(intlMessages.applauseDesc)} onClick={() => actions.setEmojiHandler('applause')} + tabIndex={-1} /> <DropdownListSeparator /> <DropdownListItem @@ -112,6 +122,7 @@ class EmojiMenu extends Component { label={intl.formatMessage(intlMessages.clearLabel)} description={intl.formatMessage(intlMessages.clearDesc)} onClick={() => actions.setEmojiHandler('none')} + tabIndex={-1} /> </DropdownList> </DropdownContent> diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index caf790869c726e4dcdaac4cf37d121a540dce274..154e5a7e3912f97879a788b1487d4f1ad5f46f7f 100755 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -31,7 +31,6 @@ const intlMessages = defineMessages({ kickedMessage: { id: 'app.error.kicked', description: 'Message when the user is kicked out of the meeting', - defaultMessage: 'You have been kicked out of the meeting', }, }); diff --git a/bigbluebutton-html5/imports/ui/components/chat/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/component.jsx index 97e6e638e48b5ec271f5aeeefeb664a59f913028..6c1cec82e9bb723415fa38b2ed420d569d87211c 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/component.jsx @@ -47,7 +47,7 @@ class Chat extends Component { <Link to="/users" role="button" - aria-label={intl.formatMessage(intlMessages.hideChatLabel, { title: title })}> + aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })}> <Icon iconName="left_arrow"/> {title} </Link> </div> @@ -58,7 +58,7 @@ class Chat extends Component { <Link to="/users" role="button" - aria-label={intl.formatMessage(intlMessages.closeChatLabel, { title: title })}> + aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}> <Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)}/> </Link>) } diff --git a/bigbluebutton-html5/imports/ui/components/chat/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/container.jsx index c7578a846596a6b56c842e60a8d239d935841c68..e4705b2976db00564f20aa027d1b1e351e93f8c6 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/container.jsx @@ -11,17 +11,14 @@ import ChatService from './service'; const intlMessages = defineMessages({ titlePublic: { id: 'app.chat.titlePublic', - defaultMessage: 'Public Chat', description: 'Public chat title', }, titlePrivate: { id: 'app.chat.titlePrivate', - defaultMessage: 'Private Chat with {name}', description: 'Private chat title', }, partnerDisconnected: { id: 'app.chat.partnerDisconnected', - defaultMessage: '{name} has left the meeting', description: 'System chat message when the private chat partnet disconnect from the meeting', }, }); @@ -65,7 +62,7 @@ export default injectIntl(createContainer(({ params, intl }) => { let userMessage = messages.find(m => m.sender !== null); let user = ChatService.getUser(chatID, '{{NAME}}'); - title = intl.formatMessage(intlMessages.titlePrivate, { name: user.name }); + title = intl.formatMessage(intlMessages.titlePrivate, { 0: user.name }); chatName = user.name; if (!user.isOnline) { @@ -75,7 +72,7 @@ export default injectIntl(createContainer(({ params, intl }) => { id, content: [{ id, - text: intl.formatMessage(intlMessages.partnerDisconnected, { name: user.name }), + text: intl.formatMessage(intlMessages.partnerDisconnected, { 0: user.name }), time, },], time, diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx index f58beaaab0890011374cb5bfa03f37ba882d3143..030ffa9cbd1cbc3d82bc1c203bdcb35ac6283237 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx @@ -105,9 +105,9 @@ class MessageForm extends Component { <TextareaAutosize className={styles.input} id="message-input" - placeholder={intl.formatMessage(messages.inputPlaceholder, { name: chatName })} + placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: chatName })} aria-controls={this.props.chatAreaId} - aria-label={intl.formatMessage(messages.inputLabel, { name: chatTitle })} + aria-label={intl.formatMessage(messages.inputLabel, { 0: chatTitle })} autoCorrect="off" autoComplete="off" spellCheck="true" diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx index 7190e0d410d1e5e6d5688841d805f9885a0e0eee..3676e902756c1eaec51e375cfde8e8074d1a2b89 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx @@ -83,15 +83,18 @@ class Dropdown extends Component { handleShow() { this.setState({ isOpen: true }, this.handleStateCallback); - - const contentElement = findDOMNode(this.refs.content); - contentElement.querySelector(FOCUSABLE_CHILDREN).focus(); } handleHide() { + + const { autoFocus } = this.props; + this.setState({ isOpen: false }, this.handleStateCallback); - const triggerElement = findDOMNode(this.refs.trigger); - triggerElement.focus(); + + if (autoFocus) { + const triggerElement = findDOMNode(this.refs.trigger); + triggerElement.focus(); + } } componentDidMount () { @@ -122,7 +125,14 @@ class Dropdown extends Component { } render() { - const { children, className, style, intl } = this.props; + const { + children, + className, + style, intl, + hasPopup, + ariaLive, + ariaRelevant, + } = this.props; let trigger = children.find(x => x.type === DropdownTrigger); let content = children.find(x => x.type === DropdownContent); @@ -143,7 +153,12 @@ class Dropdown extends Component { }); return ( - <div style={style} className={cx(styles.dropdown, className)}> + <div + style={style} + className={cx(styles.dropdown, className)} + aria-live={ariaLive} + aria-relevant={ariaRelevant} + aria-haspopup={hasPopup}> {trigger} {content} { this.state.isOpen ? diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx index c355a4fc27333f8ca175fb2540222004f6595916..b6349a23d3c0032685913c41dd236e1eefd783b9 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx @@ -78,7 +78,7 @@ export default class DropdownList extends Component { nextActiveItemIndex = this.childrenRefs.length - 1; } - if ([KEY_CODES.TAB, KEY_CODES.ESCAPE].includes(event.which)) { + if ([KEY_CODES.ESCAPE].includes(event.which)) { nextActiveItemIndex = 0; dropdownHide(); } diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx index 369904192a254ee6de176e20c439e3730eb22957..2c8e215984dafa60044d0f88fd8bfe9610585d51 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx @@ -29,14 +29,16 @@ export default class DropdownListItem extends Component { render() { const { label, description, children, injectRef, tabIndex, onClick, onKeyDown, - className, style, separator, intl, } = this.props; + className, style, separator, intl, placeInTabOrder, } = this.props; + + let index = (placeInTabOrder) ? 0 : -1; return ( <li ref={injectRef} onClick={onClick} onKeyDown={onKeyDown} - tabIndex={tabIndex} + tabIndex={index} aria-labelledby={this.labelID} aria-describedby={this.descID} className={cx(styles.item, className)} diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/title/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/title/component.jsx index 716c22896741492054e756bc6d11c5285c63f75f..96500c1df1f8b3b6cbf6b8155a56301c449a589b 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/list/title/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/title/component.jsx @@ -1,22 +1,22 @@ -import React, { Component, PropTypes } from 'react'; -import styles from '../styles'; - -const propTypes = { - description: PropTypes.string, -}; - -export default class DropdownListTitle extends Component { - - render() { - const { intl, description } = this.props; - - return ( - <div> - <li className={styles.title} aria-describedby="labelContext">{this.props.children}</li> - <div id="labelContext" aria-label={description}></div> - </div> - ); - } -} - -DropdownListTitle.propTypes = propTypes; +import React, { Component, PropTypes } from 'react'; +import styles from '../styles'; + +const propTypes = { + description: PropTypes.string, +}; + +export default class DropdownListTitle extends Component { + + render() { + const { intl, description } = this.props; + + return ( + <div> + <li className={styles.title} aria-describedby="labelContext">{this.props.children}</li> + <div id="labelContext" aria-label={description}></div> + </div> + ); + } +} + +DropdownListTitle.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/trigger/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/trigger/component.jsx index 75ede0df9c501427551f5310bced381d90b3a9d7..2df2f49f669943b7b8425dbf29ae3bc10c1aa357 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/trigger/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/trigger/component.jsx @@ -41,14 +41,16 @@ export default class DropdownTrigger extends Component { } render() { - const { children, style, className, } = this.props; + const { children, style, className, placeInTabOrder, } = this.props; const TriggerComponent = React.Children.only(children); + let index = (placeInTabOrder) ? '0' : '-1'; + const TriggerComponentBounded = React.cloneElement(children, { onClick: this.handleClick, onKeyDown: this.handleKeyDown, 'aria-haspopup': true, - tabIndex: '0', + tabIndex: index, style: style, className: cx(children.props.className, className), }); diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx index 72b3cbe9c90f70b4de536e36129de4acfc01b7a6..961625f114954c0e9082999d8bc679534449da85 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx @@ -83,8 +83,8 @@ class SettingsDropdown extends Component { } return ( - <Dropdown ref="dropdown"> - <DropdownTrigger> + <Dropdown autoFocus={true}> + <DropdownTrigger placeInTabOrder={true}> <Button label={intl.formatMessage(intlMessages.optionsLabel)} icon="more" diff --git a/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx old mode 100644 new mode 100755 index eee4b867ff67f1e4512ed36601571e72e69a80eb..6b38d1c875cf5094b1ef1749951e4555f544d147 --- a/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx @@ -27,32 +27,26 @@ const STATUS_OFFLINE = 'offline'; const intlMessages = defineMessages({ failedMessage: { id: 'app.failedMessage', - defaultMessage: 'Apologies, trouble connecting to the server.', - description: 'Message when the client is trying to connect to the server', + description: 'Notification for connecting to server problems', }, connectingMessage: { id: 'app.connectingMessage', - defaultMessage: 'Connecting...', - description: 'Message when the client is trying to connect to the server', + description: 'Notification message for when client is connecting to server', }, waitingMessage: { id: 'app.waitingMessage', - defaultMessage: 'Disconnected. Trying to reconnect in {seconds} seconds...', - description: 'Message when the client is trying to reconnect to the server', + description: 'Notification message for disconnection with reconnection counter', }, breakoutTimeRemaining: { id: 'app.breakoutTimeRemainingMessage', - defaultMessage: 'Breakout Room time remaining: {time}', description: 'Message that tells how much time is remaining for the breakout room', }, breakoutWillClose: { id: 'app.breakoutWillCloseMessage', - defaultMessage: 'Time ended. Breakout Room will close soon', description: 'Message that tells time has ended and breakout will close', }, calculatingBreakoutTimeRemaining: { id: 'app.calculatingBreakoutTimeRemaining', - defaultMessage: 'Calculating remaining time...', description: 'Message that tells that the remaining time is being calculated', }, }); @@ -146,7 +140,7 @@ export default injectIntl(createContainer(({ intl }) => { retryInterval = startCounter(sec, setRetrySeconds, getRetrySeconds, retryInterval); data.message = intl.formatMessage( intlMessages.waitingMessage, - { seconds: getRetrySeconds() } + { 0: getRetrySeconds() } ); break; } diff --git a/bigbluebutton-html5/imports/ui/components/presentation/default-content/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/default-content/component.jsx index db94fdb95357c4fae75a0c6eb43249c3a9a88e45..097b4a6e83683741d9a8fd5d5bd460c5e72a2b0d 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/default-content/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/default-content/component.jsx @@ -31,8 +31,8 @@ export default class DefaultContent extends Component { <FormattedMessage id="app.home.greeting" description="Message to greet the user." - defaultMessage="Welcome {name}! Your presentation will begin shortly..." - values={{ name: 'James Bond' }} + defaultMessage="Welcome {0}! Your presentation will begin shortly..." + values={{ 0: 'James Bond' }} /> <br/> Today is {' '}<FormattedDate value={Date.now()} /> diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx index 78f64162af94f2d29927e3a17251f56b8cbdf3c6..0ade0c7fc6ff46bf5165e887a0aff0ed17382f18 100755 --- a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx @@ -181,7 +181,7 @@ class ApplicationMenu extends BaseMenu { defaultValue={this.state.settings.locale} className={styles.select} onChange={this.handleSelectChange.bind(this, 'locale', availableLocales)}> - <option> + <option disabled={true}> { availableLocales && availableLocales.length ? intl.formatMessage(intlMessages.languageOptionLabel) : diff --git a/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx b/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx index f95e3702746238873b5ba4962d43935218544d04..bffd9d4e8e4d19027f74458c5e47a564877da90f 100755 --- a/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx @@ -33,7 +33,7 @@ export default class UserAvatar extends Component { return ( <div className={user.isOnline ? styles.userAvatar : styles.userLogout} - style={avatarStyles}> + style={avatarStyles} aria-hidden="true"> <div> {this.renderAvatarContent()} </div> diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx index c48ee6d265194e7747788a8f1809eb8b5f59c5dd..f615401d42ced4eab231f8024c9f167ef73b9dce 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx @@ -43,6 +43,7 @@ class ChatListItem extends Component { openChat, compact, intl, + tabIndex, } = this.props; const linkPath = [PRIVATE_CHAT_PATH, chat.id].join(''); @@ -57,12 +58,13 @@ class ChatListItem extends Component { } return ( - <li className={cx(styles.chatListItem, linkClasses)}> <Link to={linkPath} - className={styles.chatListItemLink} + className={cx(styles.chatListItem, linkClasses)} role="button" - aria-expanded={isCurrentChat}> + aria-expanded={isCurrentChat} + tabIndex={tabIndex}> + <div className={styles.chatListItemLink}> {chat.icon ? this.renderChatIcon() : this.renderChatAvatar()} <div className={styles.chatName}> {!compact ? <span className={styles.chatNameMain}>{chat.name}</span> : null } @@ -71,15 +73,15 @@ class ChatListItem extends Component { <div className={styles.unreadMessages} aria-label={isSingleMessage - ? intl.formatMessage(intlMessages.unreadSingular, { count: chat.unreadCounter }) - : intl.formatMessage(intlMessages.unreadPlural, { count: chat.unreadCounter })}> + ? intl.formatMessage(intlMessages.unreadSingular, { 0: chat.unreadCounter }) + : intl.formatMessage(intlMessages.unreadPlural, { 0: chat.unreadCounter })}> <div className={styles.unreadMessagesText} aria-hidden="true"> {chat.unreadCounter} </div> </div> : null} + </div> </Link> - </li> ); } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/styles.scss index 05cdca0d7b6e8f2b4e32a59119ef3315ebc4ccb5..eb2e80c3b7658e26b1246e0f54c1694285036618 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/styles.scss @@ -4,6 +4,7 @@ @extend %list-item; cursor: pointer; padding: 0; + text-decoration: none; } .chatListItemLink { diff --git a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx index 89ac3240b849f6bf3cb06836eb8b4972087bd4af..b6e161f1508844cc508b9b2870e8df61b3ad68c6 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx @@ -6,6 +6,7 @@ import cx from 'classnames'; import { defineMessages, injectIntl } from 'react-intl'; import UserListItem from './user-list-item/component.jsx'; import ChatListItem from './chat-list-item/component.jsx'; +import KEY_CODES from '/imports/utils/keyCodes'; const propTypes = { openChats: PropTypes.array.isRequired, @@ -30,6 +31,128 @@ class UserList extends Component { this.state = { compact: this.props.compact, }; + + this.rovingIndex = this.rovingIndex.bind(this); + this.focusList = this.focusList.bind(this); + this.focusListItem = this.focusListItem.bind(this); + this.counter = -1; + } + + focusList(activeElement, list) { + activeElement.tabIndex = -1; + this.counter = 0; + list.tabIndex = 0; + list.focus(); + } + + focusListItem(active, direction, element, count) { + + function select() { + element.tabIndex = 0; + element.focus(); + } + + active.tabIndex = -1; + + switch (direction) { + case 'down': + element.childNodes[this.counter].tabIndex = 0; + element.childNodes[this.counter].focus(); + this.counter++; + break; + case 'up': + this.counter--; + element.childNodes[this.counter].tabIndex = 0; + element.childNodes[this.counter].focus(); + break; + case 'upLoopUp': + case 'upLoopDown': + this.counter = count - 1; + select(); + break; + case 'downLoopDown': + this.counter = -1; + select(); + break; + case 'downLoopUp': + this.counter = 1; + select(); + break; + } + } + + rovingIndex(...Args) { + const { users, openChats } = this.props; + + let active = document.activeElement; + let list; + let items; + let count; + + switch (Args[1]) { + case 'users': + list = this._usersList; + items = this._userItems; + count = users.length; + break; + case 'messages': + list = this._msgsList; + items = this._msgItems; + count = openChats.length; + break; + } + + if (Args[0].keyCode === KEY_CODES.ESCAPE + || this.counter === -1 + || this.counter > count) { + this.focusList(active, list); + } + + if (Args[0].keyCode === KEY_CODES.ENTER + || Args[0].keyCode === KEY_CODES.ARROW_RIGHT + || Args[0].keyCode === KEY_CODES.ARROW_LEFT) { + active.firstChild.click(); + } + + if (Args[0].keyCode === KEY_CODES.ARROW_DOWN) { + if (this.counter < count) { + this.focusListItem(active, 'down', items); + }else if (this.counter === count) { + this.focusListItem(active, 'downLoopDown', list); + }else if (this.counter === 0) { + this.focusListItem(active, 'downLoopUp', list); + } + } + + if (Args[0].keyCode === KEY_CODES.ARROW_UP) { + if (this.counter < count && this.counter !== 0) { + this.focusListItem(active, 'up', items); + }else if (this.counter === 0) { + this.focusListItem(active, 'upLoopUp', list, count); + }else if (this.counter === count) { + this.focusListItem(active, 'upLoopDown', list, count); + } + } + } + + componentDidMount() { + let _this = this; + + if (!this.state.compact) { + this._msgsList.addEventListener('keypress', function (event) { + _this.rovingIndex.call(this, event, 'messages'); + }); + + this._usersList.addEventListener('keypress', function (event) { + _this.rovingIndex.call(this, event, 'users'); + }); + } + } + + componentWillUnmount() { + this._msgsList.removeEventListener('keypress', function (event) {}, false); + + this._usersList.removeEventListener('keypress', function (event) {}, false); } render() { @@ -48,9 +171,9 @@ class UserList extends Component { <div className={styles.header}> { !this.state.compact ? - <h2 className={styles.headerTitle}> + <div className={styles.headerTitle} role="banner"> {intl.formatMessage(intlMessages.participantsTitle)} - </h2> : null + </div> : null } </div> ); @@ -76,11 +199,14 @@ class UserList extends Component { <div className={styles.messages}> { !this.state.compact ? - <h3 className={styles.smallTitle}> + <div className={styles.smallTitle} role="banner"> {intl.formatMessage(intlMessages.messagesTitle)} - </h3> : <hr className={styles.separator}></hr> + </div> : <hr className={styles.separator}></hr> } - <div className={styles.scrollableList}> + <div + tabIndex={0} + className={styles.scrollableList} + ref={(r) => this._msgsList = r}> <ReactCSSTransitionGroup transitionName={listTransition} transitionAppear={true} @@ -89,15 +215,18 @@ class UserList extends Component { transitionAppearTimeout={0} transitionEnterTimeout={0} transitionLeaveTimeout={0} - component="ul" + component="div" className={cx(styles.chatsList, styles.scrollableList)}> + <div ref={(r) => this._msgItems = r}> {openChats.map(chat => ( <ChatListItem compact={this.state.compact} key={chat.id} openChat={openChat} - chat={chat} /> + chat={chat} + tabIndex={-1} /> ))} + </div> </ReactCSSTransitionGroup> </div> </div> @@ -111,6 +240,7 @@ class UserList extends Component { isBreakoutRoom, intl, makeCall, + meeting, } = this.props; const userActions = { @@ -150,33 +280,41 @@ class UserList extends Component { <div className={styles.participants}> { !this.state.compact ? - <h3 className={styles.smallTitle}> + <div className={styles.smallTitle} role="banner"> {intl.formatMessage(intlMessages.usersTitle)} ({users.length}) - </h3> : <hr className={styles.separator}></hr> + </div> : <hr className={styles.separator}></hr> } - <ReactCSSTransitionGroup - transitionName={listTransition} - transitionAppear={true} - transitionEnter={true} - transitionLeave={true} - transitionAppearTimeout={0} - transitionEnterTimeout={0} - transitionLeaveTimeout={0} - component="ul" - className={cx(styles.participantsList, styles.scrollableList)}> - { - users.map(user => ( - <UserListItem - compact={this.state.compact} - key={user.id} - isBreakoutRoom={isBreakoutRoom} - user={user} - currentUser={currentUser} - userActions={userActions} - /> - ))} - </ReactCSSTransitionGroup> + <div + className={styles.scrollableList} + tabIndex={0} + ref={(r) => this._usersList = r}> + <ReactCSSTransitionGroup + transitionName={listTransition} + transitionAppear={true} + transitionEnter={true} + transitionLeave={true} + transitionAppearTimeout={0} + transitionEnterTimeout={0} + transitionLeaveTimeout={0} + component="div" + className={cx(styles.participantsList, styles.scrollableList)}> + <div ref={(r) => this._userItems = r}> + { + users.map(user => ( + <UserListItem + compact={this.state.compact} + key={user.id} + isBreakoutRoom={isBreakoutRoom} + user={user} + currentUser={currentUser} + userActions={userActions} + meeting={meeting} + />)) + } + </div> + </ReactCSSTransitionGroup> + </div> </div> ); } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx index 6aab0c71ab632e243d4b03cb44ec9fef141966aa..a3433b154f06518a49cc05c81141aa0ad0ff1cd5 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx @@ -3,6 +3,7 @@ import { createContainer } from 'meteor/react-meteor-data'; import { meetingIsBreakout } from '/imports/ui/components/app/service'; import { makeCall } from '/imports/ui/services/api'; import Service from './service.js'; +import Meetings from '/imports/api/meetings'; import UserList from './component.jsx'; @@ -17,12 +18,14 @@ class UserListContainer extends Component { userActions, isBreakoutRoom, children, + meeting, } = this.props; return ( <UserList compact={compact} users={users} + meeting={meeting} currentUser={currentUser} openChats={openChats} openChat={openChat} @@ -37,6 +40,7 @@ class UserListContainer extends Component { export default createContainer(({ params }) => ({ users: Service.getUsers(), + meeting: Meetings.findOne({}), currentUser: Service.getCurrentUser(), openChats: Service.getOpenChats(params.chatID), openChat: params.chatID, diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx index 75b55c8609ebfec865cc81bec8e3b319b8c2c292..f7bdaf1ac7479cd3286facab571255bbaeb835b8 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx @@ -54,6 +54,14 @@ const messages = defineMessages({ id: 'app.userlist.menuTitleContext', description: 'adds context to userListItem menu title', }, + userItemStatusAriaLabel: { + id: 'app.userlist.useritem.status.arialabel', + description: 'adds aria label for user and status', + }, + userItemAriaLabel: { + id: 'app.userlist.useritem.nostatus.arialabel', + description: 'aria label for user', + }, }); const userActionsTransition = { @@ -234,15 +242,81 @@ class UserListItem extends Component { userItemContentsStyle[styles.userItemContentsCompact] = compact; userItemContentsStyle[styles.active] = this.state.isActionsOpen; + const { + user, + intl, + } = this.props; + + let you = (user.isCurrent) ? intl.formatMessage(messages.you) : null; + + let presenter = (user.isPresenter) + ? intl.formatMessage(messages.presenter) + : null; + + let userAriaLabel = (user.emoji.status === 'none') + ? intl.formatMessage(messages.userItemAriaLabel, + { username: user.name, presenter: presenter, you: you, }) + : intl.formatMessage(messages.userItemStatusAriaLabel, + { username: user.name, + presenter: presenter, + you: you, + status: user.emoji.status, }); + + let actions = this.getAvailableActions(); + let contents = ( + <div + className={cx(styles.userListItem, userItemContentsStyle)} + aria-label={userAriaLabel}> + <div className={styles.userItemContents} aria-hidden="true"> + <UserAvatar user={user} /> + {this.renderUserName()} + {this.renderUserIcons()} + </div> + </div> + ); + + if (!actions.length) { + return contents; + } + + const { dropdownOffset, dropdownDirection, dropdownVisible, } = this.state; + return ( - <li - role="button" - aria-haspopup="true" - aria-live="assertive" - aria-relevant="additions" - className={cx(styles.userListItem, userItemContentsStyle)}> - {this.renderUserContents()} - </li> + <Dropdown + ref="dropdown" + isOpen={this.state.isActionsOpen} + onShow={this.onActionsShow} + onHide={this.onActionsHide} + className={styles.dropdown} + autoFocus={false} + hasPopup="true" + ariaLive="assertive" + ariaRelevant="additions"> + <DropdownTrigger> + {contents} + </DropdownTrigger> + <DropdownContent + style={{ + visibility: dropdownVisible ? 'visible' : 'hidden', + [dropdownDirection]: `${dropdownOffset}px`, + }} + className={styles.dropdownContent} + placement={`right ${dropdownDirection}`}> + + <DropdownList> + { + [ + (<DropdownListTitle + description={intl.formatMessage(messages.menuTitleContext)} + key={_.uniqueId('dropdown-list-title')}> + {user.name} + </DropdownListTitle>), + (<DropdownListSeparator key={_.uniqueId('action-separator')} />), + ].concat(actions) + } + </DropdownList> + </DropdownContent> + </Dropdown> ); } @@ -254,7 +328,7 @@ class UserListItem extends Component { let actions = this.getAvailableActions(); let contents = ( - <div tabIndex={0} className={styles.userItemContents}> + <div className={styles.userItemContents}> <UserAvatar user={user} /> {this.renderUserName()} {this.renderUserIcons()} @@ -273,7 +347,8 @@ class UserListItem extends Component { isOpen={this.state.isActionsOpen} onShow={this.onActionsShow} onHide={this.onActionsHide} - className={styles.dropdown}> + className={styles.dropdown} + autoFocus={false}> <DropdownTrigger> {contents} </DropdownTrigger> @@ -307,6 +382,7 @@ class UserListItem extends Component { user, intl, compact, + meeting, } = this.props; if (compact) { @@ -325,6 +401,8 @@ class UserListItem extends Component { userNameSub = userNameSub.join(' '); + const { disablePrivateChat, disableCam, disableMic, lockedLayout, disablePublicChat } = meeting.roomLockSettings; + return ( <div className={styles.userName}> <span className={styles.userNameMain}> @@ -332,11 +410,15 @@ class UserListItem extends Component { </span> <span className={styles.userNameSub}> {userNameSub} - {(user.isLocked) ? - <span> {(user.isCurrent? " | " : null)} + {(user.isLocked && (disablePrivateChat + || disableCam + || disableMic + || lockedLayout + || disablePublicChat)) ? + <span> {(user.isCurrent ? ' | ' : null)} <Icon iconName='lock' /> {intl.formatMessage(messages.locked)} - </span>: null} + </span> : null} </span> </div> ); @@ -404,6 +486,7 @@ class UserListItem extends Component { label={action.label} defaultMessage={action.label} onClick={action.handler.bind(this, ...parameters)} + placeInTabOrder={true} /> ); diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/shapes/text/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/shapes/text/component.jsx index 099b1a430ff8faa5e8bb5a7c1ae94cf64a274ce2..fb581a43729d7e848d05742dcf6a2e297702adb3 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/shapes/text/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/shapes/text/component.jsx @@ -38,6 +38,7 @@ export default class TextDrawComponent extends React.Component { fontStretch: 'normal', lineHeight: 'normal', fontFamily: 'Arial', + whiteSpace: 'pre-wrap', wordWrap: 'break-word', wordBreak: 'normal', textAlign: 'left', diff --git a/bigbluebutton-html5/imports/ui/services/auth/index.js b/bigbluebutton-html5/imports/ui/services/auth/index.js index ec00d61c6ed85faf69c4e4f855a765aefe63b207..4ea2e8bc0cd9a0d12870b5e0b33ddbb74d1e5e46 100755 --- a/bigbluebutton-html5/imports/ui/services/auth/index.js +++ b/bigbluebutton-html5/imports/ui/services/auth/index.js @@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker'; import Storage from '/imports/ui/services/storage/session'; import Users from '/imports/api/users'; -import { makeCall } from '/imports/ui/services/api'; +import { makeCall, logClient } from '/imports/ui/services/api'; const CONNECTION_TIMEOUT = Meteor.settings.public.app.connectionTimeout; @@ -89,10 +89,22 @@ class Auth { } return new Promise((resolve, reject) => { - makeCall('userLogout').then(() => { - this.fetchLogoutUrl() + const credentialsSnapshot = { + meetingId: this.meetingID, + requesterUserId: this.userID, + requesterToken: this.token, + }; + + // make sure users who did not connect are not added to the meeting + // do **not** use the custom call - it relies on expired data + Meteor.call('userLogout', credentialsSnapshot, (error, result) => { + if (error) { + logClient('error', { error, method: 'userLogout', credentialsSnapshot }); + } else { + this.fetchLogoutUrl() .then(this.clearCredentials) .then(resolve); + } }); }); }; @@ -109,6 +121,13 @@ class Auth { return new Promise((resolve, reject) => { Tracker.autorun((c) => { + if (!(credentials.meetingId && credentials.requesterToken && credentials.requesterUserId)) { + return reject({ + error: 500, + description: 'Authentication subscription failed due to missing credentials.', + }); + } + setTimeout(() => { c.stop(); reject({ diff --git a/bigbluebutton-html5/imports/utils/keyCodes.js b/bigbluebutton-html5/imports/utils/keyCodes.js old mode 100644 new mode 100755 index 555e0ef1bf49204671ed80199b7a03229d2d7f83..5042c536a0124842f2aa14d201ebaeda2db89d7e --- a/bigbluebutton-html5/imports/utils/keyCodes.js +++ b/bigbluebutton-html5/imports/utils/keyCodes.js @@ -4,6 +4,8 @@ export const TAB = 9; export const ESCAPE = 27; export const ARROW_UP = 38; export const ARROW_DOWN = 40; +export const ARROW_RIGHT = 39; +export const ARROW_LEFT = 37; export default { SPACE, @@ -12,4 +14,6 @@ export default { ESCAPE, ARROW_UP, ARROW_DOWN, + ARROW_RIGHT, + ARROW_LEFT, }; diff --git a/bigbluebutton-html5/private/locales/bg_BG.json b/bigbluebutton-html5/private/locales/bg_BG.json old mode 100644 new mode 100755 index 73ceb36dea69e6899b4f4d1e39c23d5043d55fcd..575eae9805496cceede9d7f63b06d6c4544535f9 --- a/bigbluebutton-html5/private/locales/bg_BG.json +++ b/bigbluebutton-html5/private/locales/bg_BG.json @@ -1,16 +1,16 @@ { - "app.home.greeting": "Добре дошли, {name}! Вашата Ð¿Ñ€ÐµÐ·ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ Ñ‰Ðµ започне вÑеки момент", + "app.home.greeting": "Добре дошли, {0}! Вашата Ð¿Ñ€ÐµÐ·ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ Ñ‰Ðµ започне вÑеки момент", "app.userlist.usersTitle": "Потребители", "app.userlist.participantsTitle": "УчаÑтници", "app.userlist.messagesTitle": "СъобщениÑ", "app.userlist.presenter": "Лектор", "app.userlist.you": "Вие", "app.chat.submitLabel": "Изпрати", - "app.chat.inputLabel": "Въведи Ñъобщение за {name}", - "app.chat.inputPlaceholder": "Съобщение {name}", + "app.chat.inputLabel": "Въведи Ñъобщение за {0}", + "app.chat.inputPlaceholder": "Съобщение {0}", "app.chat.titlePublic": "Общ чат", - "app.chat.titlePrivate": "Private Chat with {name}", - "app.chat.partnerDisconnected": "{name} has left the meeting", + "app.chat.titlePrivate": "Private Chat with {0}", + "app.chat.partnerDisconnected": "{0} has left the meeting", "app.chat.moreMessages": "More messages below", "app.kickMessage": "You have been kicked out of the meeting", "app.whiteboard.slideControls.prevSlideLabel": "Previous slide", @@ -27,7 +27,7 @@ "app.whiteboard.slideControls.zoomDescrip": "Change the zoom level of the presentation", "app.failedMessage": "Apologies, trouble connecting to the server.", "app.connectingMessage": "Connecting...", - "app.waitingMessage": "Disconnected. Trying to reconnect in {seconds} seconds...", + "app.waitingMessage": "Disconnected. Trying to reconnect in {0} seconds...", "app.navBar.settingsDropdown.optionsLabel": "Options", "app.navBar.settingsDropdown.fullscreenLabel": "Make fullscreen", "app.navBar.settingsDropdown.settingsLabel": "Open settings", @@ -89,7 +89,7 @@ "app.breakoutJoinConfirmation.confirmDesc": "Join you to the Breakout Room", "app.breakoutJoinConfirmation.dismissLabel": "Cancel", "app.breakoutJoinConfirmation.dismissDesc": "Closes and rejects Joining the Breakout Room", - "app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {time}", + "app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {0}", "app.breakoutWillCloseMessage": "Time ended. Breakout Room will close soon", "app.calculatingBreakoutTimeRemaining": "Calculating remaining time...", "app.audioModal.microphoneLabel": "Microphone", diff --git a/bigbluebutton-html5/private/locales/de.json b/bigbluebutton-html5/private/locales/de.json old mode 100644 new mode 100755 index c6a7cba1b0065f8257be456c8075ceccc7b12f89..78eea4450b2900f2078953426fb9f4cb2c2c0a30 --- a/bigbluebutton-html5/private/locales/de.json +++ b/bigbluebutton-html5/private/locales/de.json @@ -1,5 +1,5 @@ { - "app.home.greeting": "Willkommen {name}! Ihre Präsentation startet in Kürze...", + "app.home.greeting": "Willkommen {0}! Ihre Präsentation startet in Kürze...", "app.userlist.usersTitle": "Teilnehmer", "app.userlist.participantsTitle": "Teilnehmer", "app.userlist.messagesTitle": "Nachrichten", @@ -7,17 +7,17 @@ "app.userlist.you": "Sie", "app.userlist.Label": "Teilnehmerliste", "app.chat.submitLabel": "Nachricht senden", - "app.chat.inputLabel": "Chat-Nachricht eingeben für {name}", - "app.chat.inputPlaceholder": "Nachricht an {name}", + "app.chat.inputLabel": "Chat-Nachricht eingeben für {0}", + "app.chat.inputPlaceholder": "Nachricht an {0}", "app.chat.titlePublic": "Öffentlicher Chat", - "app.chat.titlePrivate": "Privater Chat mit {name}", - "app.chat.partnerDisconnected": "{name} hat die Konferenz verlassen", - "app.chat.closeChatLabel": "Schließe {title}", - "app.chat.hideChatLabel": "Verstecke {title}", + "app.chat.titlePrivate": "Privater Chat mit {0}", + "app.chat.partnerDisconnected": "{0} hat die Konferenz verlassen", + "app.chat.closeChatLabel": "Schließe {0}", + "app.chat.hideChatLabel": "Verstecke {0}", "app.chat.moreMessages": "Weitere Nachrichten", "app.userlist.menuTitleContext": "verfügbare Optionen", - "app.userlist.chatlistitem.unreadSingular": "{count} neue Nachricht", - "app.userlist.chatlistitem.unreadPlural": "{count} neue Nachrichten", + "app.userlist.chatlistitem.unreadSingular": "{0} neue Nachricht", + "app.userlist.chatlistitem.unreadPlural": "{0} neue Nachrichten", "app.chat.Label": "Chat", "app.chat.emptyLogLabel": "Chat-Log ist leer", "app.media.Label": "Medien", @@ -100,7 +100,7 @@ "app.submenu.closedCaptions.fontColorLabel": "Schriftfarbe", "app.submenu.participants.muteAllLabel": "Alle stummschalten außer Präsentator", "app.submenu.participants.lockAllLabel": "Alle Teilnehmer sperren", - "app.submenu.participants.lockItemLabel": "Teilnehmer {lockItem}", + "app.submenu.participants.lockItemLabel": "Teilnehmer {0}", "app.submenu.participants.lockMicDesc": "Deaktiviert das Mikrofon für alle gesperrten Teilnehmer", "app.submenu.participants.lockCamDesc": "Deaktiviert die Webcam für alle gesperrten Teilnehmer", "app.submenu.participants.lockPublicChatDesc": "Deaktiviert den öffentlichen Chat für alle gesperrten Teilnehmer", @@ -158,7 +158,7 @@ "app.breakoutJoinConfirmation.confirmDesc": "Dem Breakout-Raum beitreten", "app.breakoutJoinConfirmation.dismissLabel": "Abbrechen", "app.breakoutJoinConfirmation.dismissDesc": "Beitritt zum Breakout-Raum ablehnen", - "app.breakoutTimeRemainingMessage": "Verbleibende Zeit für den Breakout-Raum: {time}", + "app.breakoutTimeRemainingMessage": "Verbleibende Zeit für den Breakout-Raum: {0}", "app.breakoutWillCloseMessage": "Zeit abgelaufen. Der Breakout-Raum wird in Kürze geschlossen", "app.calculatingBreakoutTimeRemaining": "Berechne die verbleibende Zeit...", "app.audioModal.microphoneLabel": "Mit Mikrofon", diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 2ca27397a8024dfd1815a1eb7e5905ea39c20ad4..25ca747596d3e46e643f7fdc4813a21027d3ca96 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -1,5 +1,5 @@ { - "app.home.greeting": "Welcome {name}! Your presentation will begin shortly...", + "app.home.greeting": "Welcome {0}! Your presentation will begin shortly...", "app.userlist.usersTitle": "Users", "app.userlist.participantsTitle": "Participants", "app.userlist.messagesTitle": "Messages", @@ -8,23 +8,25 @@ "app.userlist.locked": "Locked", "app.userlist.Label": "User List", "app.chat.submitLabel": "Send Message", - "app.chat.inputLabel": "Message input for chat {name}", - "app.chat.inputPlaceholder": "Message {name}", + "app.chat.inputLabel": "Message input for chat {0}", + "app.chat.inputPlaceholder": "Message {0}", "app.chat.titlePublic": "Public Chat", - "app.chat.titlePrivate": "Private Chat with {name}", - "app.chat.partnerDisconnected": "{name} has left the meeting", - "app.chat.closeChatLabel": "Close {title}", - "app.chat.hideChatLabel": "Hide {title}", + "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.userlist.menuTitleContext": "available options", - "app.userlist.chatlistitem.unreadSingular": "{count} New Message", - "app.userlist.chatlistitem.unreadPlural": "{count} New Messages", + "app.userlist.chatlistitem.unreadSingular": "{0} New Message", + "app.userlist.chatlistitem.unreadPlural": "{0} New Messages", "app.userlist.menu.chat.label": "Chat", "app.userlist.menu.clearStatus.label": "Clear Status", "app.userlist.menu.makePresenter.label": "Make Presenter", "app.userlist.menu.kickUser.label": "Kick user", "app.userlist.menu.muteUserAudio.label": "Mute user", "app.userlist.menu.unmuteUserAudio.label": "Unmute user", + "app.userlist.useritem.nostatus.arialabel": "{username} {presenter} {you}", + "app.userlist.useritem.status.arialabel": "{username} {presenter} {you} current status {status}", "app.chat.Label": "Chat", "app.chat.emptyLogLabel": "Chat log empty", "app.media.Label": "Media", @@ -43,7 +45,7 @@ "app.polling.pollingTitle": "Polling Options", "app.failedMessage": "Apologies, trouble connecting to the server.", "app.connectingMessage": "Connecting...", - "app.waitingMessage": "Disconnected. Trying to reconnect in {seconds} seconds...", + "app.waitingMessage": "Disconnected. Trying to reconnect in {0} seconds...", "app.navBar.settingsDropdown.optionsLabel": "Options", "app.navBar.settingsDropdown.fullscreenLabel": "Make fullscreen", "app.navBar.settingsDropdown.settingsLabel": "Open settings", @@ -108,7 +110,7 @@ "app.submenu.closedCaptions.fontColorLabel": "Font color", "app.submenu.participants.muteAllLabel": "Mute all except the presenter", "app.submenu.participants.lockAllLabel": "Lock all participants", - "app.submenu.participants.lockItemLabel": "Participants {lockItem}", + "app.submenu.participants.lockItemLabel": "Participants {0}", "app.submenu.participants.lockMicDesc": "Disables the microphone for all locked participants", "app.submenu.participants.lockCamDesc": "Disables the webcam for all locked participants", "app.submenu.participants.lockPublicChatDesc": "Disables public chat for all locked participants", @@ -162,7 +164,7 @@ "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 {status}", + "app.actionsBar.currentStatusDesc": "current status {0}", "app.audioNotification.audioFailedMessage": "Your audio connection failed to connect", "app.audioNotification.mediaFailedMessage": "getUserMicMedia failed, Only secure origins are allowed", "app.audioNotification.closeLabel": "Close", @@ -172,7 +174,7 @@ "app.breakoutJoinConfirmation.confirmDesc": "Join you to the Breakout Room", "app.breakoutJoinConfirmation.dismissLabel": "Cancel", "app.breakoutJoinConfirmation.dismissDesc": "Closes and rejects Joining the Breakout Room", - "app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {time}", + "app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {0}", "app.breakoutWillCloseMessage": "Time ended. Breakout Room will close soon", "app.calculatingBreakoutTimeRemaining": "Calculating remaining time...", "app.audioModal.microphoneLabel": "Microphone", diff --git a/bigbluebutton-html5/private/locales/pt_BR.json b/bigbluebutton-html5/private/locales/pt_BR.json old mode 100644 new mode 100755 index 27c70401b22b530564ee010372d56982e63d2d71..977ecac483925f8d12edb9be9891b470d91fd1bd --- a/bigbluebutton-html5/private/locales/pt_BR.json +++ b/bigbluebutton-html5/private/locales/pt_BR.json @@ -1,16 +1,16 @@ { - "app.home.greeting": "Bem-vindo {name}! Sua aprensentação começará em breve...", + "app.home.greeting": "Bem-vindo {0}! Sua aprensentação começará em breve...", "app.userlist.usersTitle": "Users", "app.userlist.participantsTitle": "Participantes", "app.userlist.messagesTitle": "Mensagens", "app.userlist.presenter": "Apresentador", "app.userlist.you": "Você", "app.chat.submitLabel": "Enviar Mensagem", - "app.chat.inputLabel": "Campo de mensagem para conversa {name}", - "app.chat.inputPlaceholder": "Message {name}", + "app.chat.inputLabel": "Campo de mensagem para conversa {0}", + "app.chat.inputPlaceholder": "Message {0}", "app.chat.titlePublic": "Conversa PublÃca", - "app.chat.titlePrivate": "Conversa Privada com {name}", - "app.chat.partnerDisconnected": "{name} saiu da sala", + "app.chat.titlePrivate": "Conversa Privada com {0}", + "app.chat.partnerDisconnected": "{0} saiu da sala", "app.chat.moreMessages": "Mais mensagens abaixo", "app.kickMessage": "Você foi expulso da apresentação", "app.whiteboard.slideControls.prevSlideLabel": "Slide Anterior", @@ -27,7 +27,7 @@ "app.whiteboard.slideControls.zoomDescrip": "Change the zoom level of the presentation", "app.failedMessage": "Desculpas, estamos com problemas para se conectar ao servidor.", "app.connectingMessage": "Conectando...", - "app.waitingMessage": "Desconectado. Tentando reconectar em {seconds} segundos...", + "app.waitingMessage": "Desconectado. Tentando reconectar em {0} segundos...", "app.navBar.settingsDropdown.optionsLabel": "Options", "app.navBar.settingsDropdown.fullscreenLabel": "Make fullscreen", "app.navBar.settingsDropdown.settingsLabel": "Open settings", @@ -89,7 +89,7 @@ "app.breakoutJoinConfirmation.confirmDesc": "Join you to the Breakout Room", "app.breakoutJoinConfirmation.dismissLabel": "Cancel", "app.breakoutJoinConfirmation.dismissDesc": "Closes and rejects Joining the Breakout Room", - "app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {time}", + "app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {0}", "app.breakoutWillCloseMessage": "Time ended. Breakout Room will close soon", "app.calculatingBreakoutTimeRemaining": "Calculating remaining time...", "app.audioModal.microphoneLabel": "Microphone",