diff --git a/bigbluebutton-html5/imports/startup/client/auth.js b/bigbluebutton-html5/imports/startup/client/auth.js index 217449049d5e0107e5d1671bc9e84de6044ed4e5..2708b676ebaaab8f45201b5a1ea93927da3870d3 100755 --- a/bigbluebutton-html5/imports/startup/client/auth.js +++ b/bigbluebutton-html5/imports/startup/client/auth.js @@ -2,7 +2,7 @@ import Auth from '/imports/ui/services/auth'; import SessionStorage from '/imports/ui/services/storage/session'; import { setCustomLogoUrl } from '/imports/ui/components/user-list/service'; import { log } from '/imports/ui/services/api'; -import deviceType from '/imports/utils/deviceType'; +import deviceInfo from '/imports/utils/deviceInfo'; // disconnected and trying to open a new connection const STATUS_CONNECTING = 'connecting'; @@ -39,7 +39,7 @@ export function joinRouteHandler(nextState, replace, callback) { Auth.set(meetingID, internalUserID, authToken, logoutUrl, sessionToken); - const path = deviceType().isPhone ? '/' : '/users'; + const path = deviceInfo.type().isPhone ? '/' : '/users'; replace({ pathname: path }); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index a1baf413617f3c629730c593cffb6f3fb8e849ef..050034e76165581db85b9dee1a0c8f7451163bf1 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -56,6 +56,9 @@ const intlMessages = defineMessages({ }, }); +const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; +const OPEN_ACTIONS_AK = SHORTCUTS_CONFIG.openActions.accesskey; + class ActionsDropdown extends Component { constructor(props) { super(props); @@ -142,7 +145,7 @@ class ActionsDropdown extends Component { return ( <Dropdown ref={(ref) => { this._dropdown = ref; }} > - <DropdownTrigger tabIndex={0} accessKey="a"> + <DropdownTrigger tabIndex={0} accessKey={OPEN_ACTIONS_AK}> <Button hideLabel aria-label={intl.formatMessage(intlMessages.actionsLabel)} diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx index 853612bd384a8af71e95359ae27891db4a9a1027..59931acdf3361ae3730f37d48e0211fd6cd77aec 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx @@ -33,6 +33,9 @@ const propTypes = { onChange: PropTypes.func.isRequired, }; +const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; +const OPEN_STATUS_AK = SHORTCUTS_CONFIG.openStatus.accesskey; + const EmojiSelect = ({ intl, options, @@ -44,7 +47,6 @@ const EmojiSelect = ({ const statusLabel = intl.formatMessage(intlMessages.statusTriggerLabel); - return ( <Dropdown autoFocus> <DropdownTrigger tabIndex={0}> @@ -60,7 +62,7 @@ const EmojiSelect = ({ size="lg" color="primary" onClick={() => null} - accessKey="s" + accessKey={OPEN_STATUS_AK} > <div id="currentStatus" hidden> { intl.formatMessage(intlMessages.currentStatusDesc, { 0: selected }) } diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx index 48f43ad49a6693229094f2653f4cc7c39060e077..cf75014f6e9273b4ec8f0c3433a02c381ccea75d 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx @@ -40,6 +40,11 @@ const defaultProps = { glow: false, }; +const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; +const JOIN_AUDIO_AK = SHORTCUTS_CONFIG.joinAudio.accesskey; +const LEAVE_AUDIO_AK = SHORTCUTS_CONFIG.leaveAudio.accesskey; +const MUTE_UNMUTE_AK = SHORTCUTS_CONFIG.toggleMute.accesskey; + const AudioControls = ({ handleToggleMuteMicrophone, handleJoinAudio, @@ -64,7 +69,7 @@ const AudioControls = ({ icon={unmute ? 'mute' : 'unmute'} size="lg" circle - accessKey="m" + accessKey={MUTE_UNMUTE_AK} /> : null} <Button className={styles.button} @@ -77,7 +82,7 @@ const AudioControls = ({ icon={join ? 'audio_off' : 'audio_on'} size="lg" circle - accessKey={join ? 'l' : 'j'} + accessKey={join ? LEAVE_AUDIO_AK : JOIN_AUDIO_AK} /> </span>); diff --git a/bigbluebutton-html5/imports/ui/components/chat/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/component.jsx index b90a40fb1f4a7590cb8488fc2ccfc57b25c7eea1..18fb18fb7b0f31655a43cc626bf5088aeaae0252 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/component.jsx @@ -23,6 +23,10 @@ const intlMessages = defineMessages({ }, }); +const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; +const HIDE_CHAT_AK = SHORTCUTS_CONFIG.hidePrivateChat.accesskey; +const CLOSE_CHAT_AK = SHORTCUTS_CONFIG.closePrivateChat.accesskey; + const Chat = (props) => { const { chatID, @@ -54,7 +58,7 @@ const Chat = (props) => { to="/users" role="button" aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })} - accessKey="h" + accessKey={HIDE_CHAT_AK} > <Icon iconName="left_arrow" /> {title} </Link> @@ -74,7 +78,7 @@ const Chat = (props) => { onClick={() => actions.handleClosePrivateChat(chatID)} aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })} label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })} - accessKey="g" + accessKey={CLOSE_CHAT_AK} /> </Link> : <ChatDropdown /> diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 3fbd14a2dd6b3bb6fd76dbb3fd2a8c2fa4251d84..d7c8b212decae00e64ea58e48eabdc1ae18ff7a8 100644 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -55,14 +55,17 @@ const defaultProps = { beingRecorded: false, }; +const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; +const TOGGLE_USERLIST_AK = SHORTCUTS_CONFIG.toggleUserList.accesskey; + const openBreakoutJoinConfirmation = (breakoutURL, breakoutName, mountModal) => mountModal(<BreakoutJoinConfirmation breakoutURL={breakoutURL} breakoutName={breakoutName} />); -const closeBreakoutJoinConfirmation = (mountModal) => - mountModal(null); +const closeBreakoutJoinConfirmation = mountModal => + mountModal(null); class NavBar extends Component { constructor(props) { @@ -174,8 +177,11 @@ class NavBar extends Component { /> ); } + render() { - const { hasUnreadMessages, beingRecorded, isExpanded, intl } = this.props; + const { + hasUnreadMessages, beingRecorded, isExpanded, intl, + } = this.props; const recordingMessage = beingRecorded.recording ? 'recordingIndicatorOn' : 'recordingIndicatorOff'; @@ -198,7 +204,7 @@ class NavBar extends Component { className={cx(toggleBtnClasses)} aria-expanded={isExpanded} aria-describedby="newMessage" - accessKey="u" + accessKey={TOGGLE_USERLIST_AK} /> <div id="newMessage" 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 c73bbe8ea4fc40506f9adbf0a749ba9485d1e3d7..85f9a6268550d76f468e7dab3143c3291250a472 100644 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx @@ -15,6 +15,7 @@ import DropdownContent from '/imports/ui/components/dropdown/content/component'; import DropdownList from '/imports/ui/components/dropdown/list/component'; import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component'; +import ShortcutHelpComponent from '/imports/ui/components/shortcut-help/component'; import { styles } from '../styles'; @@ -63,8 +64,19 @@ const intlMessages = defineMessages({ id: 'app.navBar.settingsDropdown.exitFullscreenLabel', description: 'Exit fullscreen option label', }, + hotkeysLabel: { + id: 'app.navBar.settingsDropdown.hotkeysLabel', + description: 'Hotkeys options label', + }, + hotkeysDesc: { + id: 'app.navBar.settingsDropdown.hotkeysDesc', + description: 'Describes hotkeys option', + }, }); +const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; +const OPEN_OPTIONS_AK = SHORTCUTS_CONFIG.openOptions.accesskey; + class SettingsDropdown extends Component { constructor(props) { super(props); @@ -113,6 +125,13 @@ class SettingsDropdown extends Component { description={intl.formatMessage(intlMessages.aboutDesc)} onClick={() => mountModal(<AboutContainer />)} />), + (<DropdownListItem + key={_.uniqueId('list-item-')} + icon="about" + label={intl.formatMessage(intlMessages.hotkeysLabel)} + description={intl.formatMessage(intlMessages.hotkeysDesc)} + onClick={() => mountModal(<ShortcutHelpComponent />)} + />), (<DropdownListSeparator key={_.uniqueId('list-separator-')} />), (<DropdownListItem key={_.uniqueId('list-item-')} @@ -154,7 +173,7 @@ class SettingsDropdown extends Component { onShow={this.onActionsShow} onHide={this.onActionsHide} > - <DropdownTrigger tabIndex={0} accessKey="o"> + <DropdownTrigger tabIndex={0} accessKey={OPEN_OPTIONS_AK}> <Button label={intl.formatMessage(intlMessages.optionsLabel)} icon="more" diff --git a/bigbluebutton-html5/imports/ui/components/shortcut-help/component.jsx b/bigbluebutton-html5/imports/ui/components/shortcut-help/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..20041887c9a340a8ee0bcada67038e9c89cbfad5 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/shortcut-help/component.jsx @@ -0,0 +1,130 @@ +import React, { Component } from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import Modal from '/imports/ui/components/modal/simple/component'; +import deviceInfo from '/imports/utils/deviceInfo'; +import _ from 'lodash'; +import { styles } from './styles'; + +const intlMessages = defineMessages({ + title: { + id: 'app.shortcut-help.title', + description: 'modal title label', + }, + closeLabel: { + id: 'app.shortcut-help.closeLabel', + description: 'label for close button', + }, + closeDesc: { + id: 'app.shortcut-help.closeDesc', + description: 'description for close button', + }, + accessKeyNotAvailable: { + id: 'app.shortcut-help.accessKeyNotAvailable', + description: 'message shown in place of access key table if not supported', + }, + comboLabel: { + id: 'app.shortcut-help.comboLabel', + description: 'heading for key combo column', + }, + functionLabel: { + id: 'app.shortcut-help.functionLabel', + description: 'heading for shortcut function column', + }, + openOptions: { + id: 'app.shortcut-help.openOptions', + description: 'describes the open options shortcut', + }, + toggleUserList: { + id: 'app.shortcut-help.toggleUserList', + description: 'describes the toggle userlist shortcut', + }, + toggleMute: { + id: 'app.shortcut-help.toggleMute', + description: 'describes the toggle mute shortcut', + }, + togglePublicChat: { + id: 'app.shortcut-help.togglePublicChat', + description: 'describes the toggle public chat shortcut', + }, + hidePrivateChat: { + id: 'app.shortcut-help.hidePrivateChat', + description: 'describes the hide public chat shortcut', + }, + closePrivateChat: { + id: 'app.shortcut-help.closePrivateChat', + description: 'describes the close private chat shortcut', + }, + openActions: { + id: 'app.shortcut-help.openActions', + description: 'describes the open actions shortcut', + }, + openStatus: { + id: 'app.shortcut-help.openStatus', + description: 'describes the open status shortcut', + }, + joinAudio: { + id: 'app.audio.joinAudio', + description: 'describes the join audio shortcut', + }, + leaveAudio: { + id: 'app.audio.leaveAudio', + description: 'describes the leave audio shortcut', + }, +}); + +const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; + +class ShortcutHelpComponent extends Component { + render() { + const { intl } = this.props; + const { isWindows, isLinux, isMac } = deviceInfo.osType(); + const { isFirefox, isChrome, isIE } = deviceInfo.browserType(); + const shortcuts = Object.values(SHORTCUTS_CONFIG); + + let accessMod = null; + + if (isMac) { + accessMod = 'Control + Alt'; + } + + if (isWindows) { + accessMod = isIE ? 'Alt' : accessMod; + } + + if (isWindows || isLinux) { + accessMod = isFirefox ? 'Alt + Shift' : accessMod; + accessMod = isChrome ? 'Alt' : accessMod; + } + + return ( + <Modal + title={intl.formatMessage(intlMessages.title)} + dismiss={{ + label: intl.formatMessage(intlMessages.closeLabel), + description: intl.formatMessage(intlMessages.closeDesc), + }} + > + { !accessMod ? <p>{intl.formatMessage(intlMessages.accessKeyNotAvailable)}</p> : + <span> + <table className={styles.shortcutTable}> + <tbody> + <tr> + <th>{intl.formatMessage(intlMessages.comboLabel)}</th> + <th>{intl.formatMessage(intlMessages.functionLabel)}</th> + </tr> + {shortcuts.map(shortcut => ( + <tr key={_.uniqueId('hotkey-item-')}> + <td className={styles.keyCell}>{`${accessMod} + ${shortcut.accesskey}`}</td> + <td className={styles.descCell}>{intl.formatMessage(intlMessages[`${shortcut.descId}`])}</td> + </tr> + ))} + </tbody> + </table> + </span> + } + </Modal> + ); + } +} + +export default injectIntl(ShortcutHelpComponent); diff --git a/bigbluebutton-html5/imports/ui/components/shortcut-help/styles.scss b/bigbluebutton-html5/imports/ui/components/shortcut-help/styles.scss new file mode 100644 index 0000000000000000000000000000000000000000..40777c10f5490bc0ca5894e0f72ded85e53aa9ab --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/shortcut-help/styles.scss @@ -0,0 +1,27 @@ +@import "/imports/ui/stylesheets/variables/_all"; + +.shortcutTable, .keyCell, .descCell, .tableTitle { + border: $border-size solid $color-gray-lighter; +} + +.keyCell { + text-align: center; +} + +.descCell { + padding-right: $jumbo-padding-x !important; +} + +.keyCell, .descCell { + padding: $sm-padding-x; + margin: auto; +} + +.shortcutTable { + border-collapse: collapse; + margin: auto; +} + +.tableTitle { + padding: $sm-padding-x; +} 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 6c302a71fb8a2a932b595c3c751599151e997745..824c2c59b758a2c3082531babb61b29a58db8892 100644 --- 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 @@ -27,6 +27,9 @@ const CHAT_CONFIG = Meteor.settings.public.chat; const PRIVATE_CHAT_PATH = CHAT_CONFIG.path_route; const CLOSED_CHAT_PATH = 'users/'; +const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; +const TOGGLE_CHAT_PUB_AK = SHORTCUTS_CONFIG.togglePublicChat.accesskey; + const propTypes = { chat: PropTypes.shape({ id: PropTypes.string.isRequired, @@ -71,7 +74,7 @@ const ChatListItem = (props) => { role="button" aria-expanded={isCurrentChat} tabIndex={tabIndex} - accessKey={isPublicChat(chat) ? 'p' : null} + accessKey={isPublicChat(chat) ? TOGGLE_CHAT_PUB_AK : null} > <div className={styles.chatListItemLink}> <div className={styles.chatIcon}> diff --git a/bigbluebutton-html5/imports/utils/deviceInfo.js b/bigbluebutton-html5/imports/utils/deviceInfo.js new file mode 100644 index 0000000000000000000000000000000000000000..4bcf40b3612107895300a2d6ebc229bab797d194 --- /dev/null +++ b/bigbluebutton-html5/imports/utils/deviceInfo.js @@ -0,0 +1,36 @@ +const deviceInfo = { + type() { + // Listing of Device Viewport sizes, Updated : March 25th, 2018 + // http://mediag.com/news/popular-screen-resolutions-designing-for-all/ + const MAX_PHONE_SHORT_SIDE = 480; + + const smallSide = window.screen.width < window.screen.height + ? window.screen.width + : window.screen.height; + + return { + isPhone: smallSide <= MAX_PHONE_SHORT_SIDE, + }; + }, + browserType() { + return { + // Uses features to determine browser + isChrome: !!window.chrome && !!window.chrome.webstore, + isFirefox: typeof InstallTrigger !== 'undefined', + isIE: 'ActiveXObject' in window, + }; + }, + osType() { + return { + // Uses userAgent to determine operating system + isWindows: window.navigator.userAgent.indexOf('Windows') !== -1, + isMac: window.navigator.userAgent.indexOf('Mac') !== -1, + isLinux: window.navigator.userAgent.indexOf('Linux') !== -1, + }; + }, + +}; + + +export default deviceInfo; + diff --git a/bigbluebutton-html5/imports/utils/deviceType.js b/bigbluebutton-html5/imports/utils/deviceType.js deleted file mode 100644 index 478e8bfa72fc63170871400b071e1560ff7c368d..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/utils/deviceType.js +++ /dev/null @@ -1,16 +0,0 @@ -const deviceType = () => { - // Listing of Device Viewport sizes, Updated : March 25th, 2018 - // http://mediag.com/news/popular-screen-resolutions-designing-for-all/ - const MAX_PHONE_SHORT_SIDE = 480; - - const smallSide = window.screen.width < window.screen.height - ? window.screen.width - : window.screen.height; - - return { - isPhone: smallSide <= MAX_PHONE_SHORT_SIDE, - }; -}; - -export default deviceType; - diff --git a/bigbluebutton-html5/private/config/settings-development.json b/bigbluebutton-html5/private/config/settings-development.json index d1f79b28d2ff177cb1ea97ed8ec75abfcd0a789d..8063b4450f003e5b671f226b01c0a8ba84e1675d 100755 --- a/bigbluebutton-html5/private/config/settings-development.json +++ b/bigbluebutton-html5/private/config/settings-development.json @@ -47,6 +47,18 @@ "layout": false } }, + "shortcuts": { + "openOptions": {"accesskey": "O", "descId": "openOptions"}, + "toggleUserList": {"accesskey": "U", "descId": "toggleUserList"}, + "toggleMute": {"accesskey": "M", "descId": "toggleMute"}, + "joinAudio": {"accesskey": "J", "descId": "joinAudio"}, + "leaveAudio": {"accesskey": "L", "descId": "leaveAudio"}, + "togglePublicChat": {"accesskey": "P", "descId": "togglePublicChat"}, + "hidePrivateChat": {"accesskey": "H", "descId": "hidePrivateChat"}, + "closePrivateChat": {"accesskey": "G", "descId": "closePrivateChat"}, + "openActions": {"accesskey": "A", "descId": "openActions"}, + "openStatus": {"accesskey": "S", "descId": "openStatus"} + }, "branding": { "displayBrandingArea": false }, diff --git a/bigbluebutton-html5/private/config/settings-production.json b/bigbluebutton-html5/private/config/settings-production.json index 04fbd9a1295481e90773ce087cd8a34776b81ad3..5497ce31c1ffe995c5cfd09fcb2e0b0286855695 100755 --- a/bigbluebutton-html5/private/config/settings-production.json +++ b/bigbluebutton-html5/private/config/settings-production.json @@ -47,6 +47,18 @@ "layout": false } }, + "shortcuts": { + "openOptions": {"accesskey": "O", "descId": "openOptions"}, + "toggleUserList": {"accesskey": "U", "descId": "toggleUserList"}, + "toggleMute": {"accesskey": "M", "descId": "toggleMute"}, + "joinAudio": {"accesskey": "J", "descId": "joinAudio"}, + "leaveAudio": {"accesskey": "L", "descId": "leaveAudio"}, + "togglePublicChat": {"accesskey": "P", "descId": "togglePublicChat"}, + "hidePrivateChat": {"accesskey": "H", "descId": "hidePrivateChat"}, + "closePrivateChat": {"accesskey": "G", "descId": "closePrivateChat"}, + "openActions": {"accesskey": "A", "descId": "openActions"}, + "openStatus": {"accesskey": "S", "descId": "openStatus"} + }, "branding": { "displayBrandingArea": false }, diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 451548ec081a78cf7771834ea9029fa037591cc1..d1b90b7be55706f7d446e7044d4cae7cfc9df3cd 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -94,6 +94,8 @@ "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": "Hotkeys", + "app.navBar.settingsDropdown.hotkeysDesc": "Listing of available hotkeys", "app.navBar.userListToggleBtnLabel": "User List Toggle", "app.navBar.toggleUserList.ariaLabel": "Users and Messages Toggle", "app.navBar.toggleUserList.newMessages": "with new message notification", @@ -294,6 +296,20 @@ "app.toast.chat.plural": "you have {0} new messages in {1}", "app.notification.recordingStart": "This session is now being recorded", "app.notification.recordingStop": "This session is not being recorded anymore", + "app.shortcut-help.title": "Hotkeys", + "app.shortcut-help.accessKeyNotAvailable": "Access keys not available.", + "app.shortcut-help.comboLabel": "Combo", + "app.shortcut-help.functionLabel": "Function", + "app.shortcut-help.closeLabel": "Close", + "app.shortcut-help.closeDesc": "Closes the hotkeys 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 (UserList 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.openStatus": "Open Status Menu", "app.video.joinVideo": "Share Webcam", "app.video.leaveVideo": "Unshare Webcam", "app.video.iceCandidateError": "Error on adding ice candidate",