[plugin-sdk-camera-settings] - changes in review

This commit is contained in:
GuiLeme 2023-09-25 21:50:06 -03:00
commit 4129215741
26 changed files with 757 additions and 67 deletions

View File

@ -252,7 +252,7 @@ jobs:
apt --purge -y remove apache2-bin
'
- name: Install BBB
timeout-minutes: 15
timeout-minutes: 25
run: |
sudo -i <<EOF
set -e

View File

@ -1,6 +1,7 @@
import React, { PureComponent } from 'react';
import CaptionsButtonContainer from '/imports/ui/components/captions/button/container';
import deviceInfo from '/imports/utils/deviceInfo';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import Styled from './styles';
import ActionsDropdown from './actions-dropdown/container';
import AudioCaptionsButtonContainer from '/imports/ui/components/audio/captions/button/container';
@ -12,6 +13,7 @@ import JoinVideoOptionsContainer from '../video-provider/video-button/container'
import PresentationOptionsContainer from './presentation-options/component';
import RaiseHandDropdownContainer from './raise-hand/container';
import { isPresentationEnabled } from '/imports/ui/services/features';
import Button from '/imports/ui/components/common/button/component';
class ActionsBar extends PureComponent {
constructor(props) {
@ -24,12 +26,53 @@ class ActionsBar extends PureComponent {
this.setCaptionsReaderMenuModalIsOpen = this.setCaptionsReaderMenuModalIsOpen.bind(this);
this.setRenderRaiseHand = this.renderRaiseHand.bind(this);
this.actionsBarRef = React.createRef();
this.renderPluginsActionBarItems = this.renderPluginsActionBarItems.bind(this);
}
setCaptionsReaderMenuModalIsOpen(value) {
this.setState({ isCaptionsReaderMenuModalOpen: value })
}
renderPluginsActionBarItems(position) {
const { actionBarItems } = this.props;
return (
<>
{
actionBarItems.filter((plugin) => plugin.position === position).map((plugin, index) => {
let actionBarItemToReturn;
switch (plugin.type) {
case PluginSdk.ActionsBarItemType.BUTTON:
actionBarItemToReturn = (
<Button
key={`${plugin.type}-${plugin.id}`}
onClick={plugin.onClick}
hideLabel
color="primary"
icon={plugin.icon}
size="lg"
circle
label={plugin.tooltip}
/>
);
break;
case PluginSdk.ActionsBarItemType.SEPARATOR:
actionBarItemToReturn = (
<Styled.Separator
key={`${plugin.type}-${plugin.id}`}
/>
);
break;
default:
actionBarItemToReturn = null;
break;
}
return actionBarItemToReturn;
})
}
</>
);
}
renderRaiseHand() {
const {
isReactionsButtonEnabled, isRaiseHandButtonEnabled, setEmojiStatus, currentUser, intl,
@ -138,6 +181,7 @@ class ActionsBar extends PureComponent {
: null }
</Styled.Left>
<Styled.Center>
{this.renderPluginsActionBarItems(PluginSdk.ActionsBarPosition.LEFT)}
<AudioControlsContainer />
{enableVideo
? (
@ -149,7 +193,8 @@ class ActionsBar extends PureComponent {
isMeteorConnected,
}}
/>
{isRaiseHandButtonCentered && this.renderRaiseHand()}
{isRaiseHandButtonCentered && this.renderRaiseHand()}
{this.renderPluginsActionBarItems(PluginSdk.ActionsBarPosition.RIGHT)}
</Styled.Center>
<Styled.Right>
{ shouldShowOptionsButton ?

View File

@ -16,6 +16,7 @@ import TimerService from '/imports/ui/components/timer/service';
import { layoutSelectOutput, layoutDispatch } from '../layout/context';
import { isExternalVideoEnabled, isPollingEnabled, isPresentationEnabled } from '/imports/ui/services/features';
import { isScreenBroadcasting, isCameraAsContentBroadcasting } from '/imports/ui/components/screenshare/service';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
import MediaService from '../media/service';
@ -24,6 +25,16 @@ const ActionsBarContainer = (props) => {
const layoutContextDispatch = layoutDispatch();
const usingUsersContext = useContext(UsersContext);
const {
pluginsProvidedAggregatedState,
} = useContext(PluginsContext);
let actionBarItems = [];
if (pluginsProvidedAggregatedState.actionsBarItems) {
actionBarItems = [
...pluginsProvidedAggregatedState.actionsBarItems,
];
}
const { users } = usingUsersContext;
const currentUser = { userId: Auth.userID, emoji: users[Auth.meetingID][Auth.userID].emoji };
@ -40,6 +51,7 @@ const ActionsBarContainer = (props) => {
layoutContextDispatch,
actionsBarStyle,
amIPresenter,
actionBarItems,
}
}
/>

View File

@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/ban-types */
import React, { useCallback } from 'react';
import React, { useCallback, useContext } from 'react';
import deviceInfo from '/imports/utils/deviceInfo';
import { defineMessages, useIntl } from 'react-intl';
import { useShortcut } from '/imports/ui/core/hooks/useShortcut';
import BBBMenu from '/imports/ui/components/common/menu/component';
import { MenuSeparatorItemType, MenuOptionItemType } from '/imports/ui/components/common/menu/menuTypes';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import Styled from '../styles';
import {
handleLeaveAudio,
@ -15,6 +16,7 @@ import {
} from '../service';
import Mutetoggle from './muteToggle';
import ListenOnly from './listenOnly';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
const AUDIO_INPUT = 'audioinput';
const AUDIO_OUTPUT = 'audiooutput';
@ -89,6 +91,16 @@ export const LiveSelection: React.FC<LiveSelectionProps> = ({
const leaveAudioShourtcut = useShortcut('leaveAudio');
const {
pluginsProvidedAggregatedState,
} = useContext(PluginsContext);
let audioSettingsDropdownItems = [] as PluginSdk.AudioSettingsDropdownItem[];
if (pluginsProvidedAggregatedState.audioSettingsDropdownItems) {
audioSettingsDropdownItems = [
...pluginsProvidedAggregatedState.audioSettingsDropdownItems,
];
}
const renderDeviceList = useCallback((
deviceKind: string,
list: MediaDeviceInfo[],
@ -194,6 +206,33 @@ export const LiveSelection: React.FC<LiveSelectionProps> = ({
isSeparator: true,
})
.concat(leaveAudioOption);
audioSettingsDropdownItems.forEach((audioSettingsDropdownItem:
PluginSdk.AudioSettingsDropdownItem) => {
switch (audioSettingsDropdownItem.type) {
case PluginSdk.AudioSettingsDropdownItemType.OPTION: {
const audioSettingsDropdownOption = audioSettingsDropdownItem as PluginSdk.AudioSettingsDropdownOption;
dropdownListComplete.push({
label: audioSettingsDropdownOption.label,
iconRight: audioSettingsDropdownOption.icon,
onClick: audioSettingsDropdownOption.onClick,
key: audioSettingsDropdownOption.id,
});
break;
}
case PluginSdk.AudioSettingsDropdownItemType.SEPARATOR: {
const audioSettingsDropdownSeparator = audioSettingsDropdownItem as PluginSdk.AudioSettingsDropdownOption;
dropdownListComplete.push({
isSeparator: true,
key: audioSettingsDropdownSeparator.id,
});
break;
}
default:
break;
}
});
const customStyles = { top: '-1rem' };
const { isMobile } = deviceInfo;
return (

View File

@ -2,17 +2,19 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import { defineMessages, injectIntl } from 'react-intl';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import Styled from './styles';
import RecordingIndicator from './recording-indicator/container';
import TalkingIndicatorContainer from '/imports/ui/components/nav-bar/talking-indicator/container';
import ConnectionStatusButton from '/imports/ui/components/connection-status/button/container';
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
import { addNewAlert } from '/imports/ui/components/screenreader-alert/service';
import SettingsDropdownContainer from './settings-dropdown/container';
import OptionsDropdownContainer from './options-dropdown/container';
import TimerIndicatorContainer from '/imports/ui/components/timer/indicator/container';
import browserInfo from '/imports/utils/browserInfo';
import deviceInfo from '/imports/utils/deviceInfo';
import { PANELS, ACTIONS } from '../layout/enums';
import Button from '/imports/ui/components/common/button/component';
import { isEqual } from 'radash';
const intlMessages = defineMessages({
@ -45,6 +47,9 @@ const propTypes = {
breakoutNum: PropTypes.number,
breakoutName: PropTypes.string,
meetingName: PropTypes.string,
pluginNavBarItems: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
})).isRequired,
};
const defaultProps = {
@ -53,6 +58,70 @@ const defaultProps = {
shortcuts: '',
};
const renderPluginItems = (pluginItems) => (
<>
{
pluginItems.map((pluginItem) => {
let returnComponent;
switch (pluginItem.type) {
case PluginSdk.NavBarItemType.BUTTON:
returnComponent = (
<Styled.PluginComponentWrapper
key={pluginItem.id}
>
<Button
icon={pluginItem.icon}
label={pluginItem.label}
aria-label={pluginItem.tooltip}
color="primary"
tooltip={pluginItem.tooltip}
onClick={pluginItem.onClick}
/>
</Styled.PluginComponentWrapper>
);
break;
case PluginSdk.NavBarItemType.INFO:
returnComponent = (
<Styled.PluginComponentWrapper
key={pluginItem.id}
tooltip={pluginItem.tooltip}
>
<Styled.PluginInfoComponent>
{pluginItem.label}
</Styled.PluginInfoComponent>
</Styled.PluginComponentWrapper>
);
break;
default:
returnComponent = null;
break;
}
if (pluginItem.hasSeparator) {
switch (pluginItem.position) {
case PluginSdk.NavBarItemPosition.RIGHT:
returnComponent = (
<>
{returnComponent}
<Styled.PluginSeparatorWrapper>|</Styled.PluginSeparatorWrapper>
</>
);
break;
default:
returnComponent = (
<>
<Styled.PluginSeparatorWrapper>|</Styled.PluginSeparatorWrapper>
{returnComponent}
</>
);
break;
}
}
return returnComponent;
})
}
</>
);
class NavBar extends Component {
constructor(props) {
super(props);
@ -62,6 +131,7 @@ class NavBar extends Component {
}
this.handleToggleUserList = this.handleToggleUserList.bind(this);
this.splitPluginItems = this.splitPluginItems.bind(this);
}
componentDidMount() {
@ -156,6 +226,31 @@ class NavBar extends Component {
}
}
splitPluginItems() {
const { pluginNavBarItems } = this.props;
return pluginNavBarItems.reduce((result, item) => {
switch (item.position) {
case PluginSdk.NavBarItemPosition.LEFT:
result.leftPluginItems.push(item);
break;
case PluginSdk.NavBarItemPosition.CENTER:
result.centerPluginItems.push(item);
break;
case PluginSdk.NavBarItemPosition.RIGHT:
result.rightPluginItems.push(item);
break;
default:
break;
}
return result;
}, {
leftPluginItems: [],
centerPluginItems: [],
rightPluginItems: [],
});
}
render() {
const {
hasUnreadMessages,
@ -189,6 +284,8 @@ class NavBar extends Component {
}
});
const { leftPluginItems, centerPluginItems, rightPluginItems } = this.splitPluginItems();
return (
<Styled.Navbar
id="Navbar"
@ -233,6 +330,7 @@ class NavBar extends Component {
&& <Styled.ArrowRight iconName="right_arrow" />}
{isExpanded && document.dir === 'rtl'
&& <Styled.ArrowRight iconName="right_arrow" />}
{renderPluginItems(leftPluginItems)}
</Styled.Left>
<Styled.Center>
<Styled.PresentationTitle data-test="presentationTitle">
@ -242,10 +340,12 @@ class NavBar extends Component {
amIModerator={amIModerator}
currentUserId={currentUserId}
/>
{renderPluginItems(centerPluginItems)}
</Styled.Center>
<Styled.Right>
{renderPluginItems(rightPluginItems)}
{ConnectionStatusService.isEnabled() ? <ConnectionStatusButton /> : null}
<SettingsDropdownContainer amIModerator={amIModerator} />
<OptionsDropdownContainer amIModerator={amIModerator} />
</Styled.Right>
</Styled.Top>
<Styled.Bottom>

View File

@ -12,6 +12,7 @@ import { UsersContext } from '/imports/ui/components/components-data/users-conte
import NotesService from '/imports/ui/components/notes/service';
import NavBar from './component';
import { layoutSelectInput, layoutSelectOutput, layoutDispatch } from '../layout/context';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
import { PANELS } from '/imports/ui/components/layout/enums';
const PUBLIC_CONFIG = Meteor.settings.public;
@ -31,6 +32,7 @@ const checkUnreadMessages = ({
const NavBarContainer = ({ children, ...props }) => {
const usingChatContext = useContext(ChatContext);
const usingUsersContext = useContext(UsersContext);
const { pluginsProvidedAggregatedState } = useContext(PluginsContext);
const usingGroupChatContext = useContext(GroupChatContext);
const { chats: groupChatsMessages } = usingChatContext;
const { users } = usingUsersContext;
@ -62,6 +64,13 @@ const NavBarContainer = ({ children, ...props }) => {
if (hideNavBar || navBar.display === false) return null;
let pluginNavBarItems = [];
if (pluginsProvidedAggregatedState.navBarItems) {
pluginNavBarItems = [
...pluginsProvidedAggregatedState.navBarItems,
];
}
return (
<NavBar
{...{
@ -76,6 +85,7 @@ const NavBarContainer = ({ children, ...props }) => {
isExpanded,
activeChats,
currentUserId: Auth.userID,
pluginNavBarItems,
...rest,
}}
style={{ ...navBar }}
@ -124,4 +134,4 @@ export default withTracker(() => {
meetingName,
unread,
};
})(NavBarContainer);
})(NavBarContainer);

View File

@ -5,87 +5,88 @@ import EndMeetingConfirmationContainer from '/imports/ui/components/end-meeting-
import { makeCall } from '/imports/ui/services/api';
import AboutContainer from '/imports/ui/components/about/container';
import MobileAppModal from '/imports/ui/components/mobile-app-modal/container';
import SettingsMenuContainer from '/imports/ui/components/settings/container';
import OptionsMenuContainer from '/imports/ui/components/settings/container';
import BBBMenu from '/imports/ui/components/common/menu/component';
import ShortcutHelpComponent from '/imports/ui/components/shortcut-help/component';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import { colorDanger, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import Styled from './styles';
import browserInfo from '/imports/utils/browserInfo';
import deviceInfo from '/imports/utils/deviceInfo';
const intlMessages = defineMessages({
optionsLabel: {
id: 'app.navBar.settingsDropdown.optionsLabel',
id: 'app.navBar.optionsDropdown.optionsLabel',
description: 'Options button label',
},
fullscreenLabel: {
id: 'app.navBar.settingsDropdown.fullscreenLabel',
id: 'app.navBar.optionsDropdown.fullscreenLabel',
description: 'Make fullscreen option label',
},
settingsLabel: {
id: 'app.navBar.settingsDropdown.settingsLabel',
id: 'app.navBar.optionsDropdown.settingsLabel',
description: 'Open settings option label',
},
aboutLabel: {
id: 'app.navBar.settingsDropdown.aboutLabel',
id: 'app.navBar.optionsDropdown.aboutLabel',
description: 'About option label',
},
aboutDesc: {
id: 'app.navBar.settingsDropdown.aboutDesc',
id: 'app.navBar.optionsDropdown.aboutDesc',
description: 'Describes about option',
},
leaveSessionLabel: {
id: 'app.navBar.settingsDropdown.leaveSessionLabel',
id: 'app.navBar.optionsDropdown.leaveSessionLabel',
description: 'Leave session button label',
},
fullscreenDesc: {
id: 'app.navBar.settingsDropdown.fullscreenDesc',
id: 'app.navBar.optionsDropdown.fullscreenDesc',
description: 'Describes fullscreen option',
},
settingsDesc: {
id: 'app.navBar.settingsDropdown.settingsDesc',
id: 'app.navBar.optionsDropdown.settingsDesc',
description: 'Describes settings option',
},
leaveSessionDesc: {
id: 'app.navBar.settingsDropdown.leaveSessionDesc',
id: 'app.navBar.optionsDropdown.leaveSessionDesc',
description: 'Describes leave session option',
},
exitFullscreenDesc: {
id: 'app.navBar.settingsDropdown.exitFullscreenDesc',
id: 'app.navBar.optionsDropdown.exitFullscreenDesc',
description: 'Describes exit fullscreen option',
},
exitFullscreenLabel: {
id: 'app.navBar.settingsDropdown.exitFullscreenLabel',
id: 'app.navBar.optionsDropdown.exitFullscreenLabel',
description: 'Exit fullscreen option label',
},
hotkeysLabel: {
id: 'app.navBar.settingsDropdown.hotkeysLabel',
id: 'app.navBar.optionsDropdown.hotkeysLabel',
description: 'Hotkeys options label',
},
hotkeysDesc: {
id: 'app.navBar.settingsDropdown.hotkeysDesc',
id: 'app.navBar.optionsDropdown.hotkeysDesc',
description: 'Describes hotkeys option',
},
helpLabel: {
id: 'app.navBar.settingsDropdown.helpLabel',
id: 'app.navBar.optionsDropdown.helpLabel',
description: 'Help options label',
},
openAppLabel: {
id: 'app.navBar.settingsDropdown.openAppLabel',
id: 'app.navBar.optionsDropdown.openAppLabel',
description: 'Open mobile app label',
},
helpDesc: {
id: 'app.navBar.settingsDropdown.helpDesc',
id: 'app.navBar.optionsDropdown.helpDesc',
description: 'Describes help option',
},
endMeetingLabel: {
id: 'app.navBar.settingsDropdown.endMeetingLabel',
id: 'app.navBar.optionsDropdown.endMeetingLabel',
description: 'End meeting options label',
},
endMeetingDesc: {
id: 'app.navBar.settingsDropdown.endMeetingDesc',
id: 'app.navBar.optionsDropdown.endMeetingDesc',
description: 'Describes settings option closing the current meeting',
},
startCaption: {
@ -113,6 +114,10 @@ const propTypes = {
audioCaptionsActive: PropTypes.bool.isRequired,
audioCaptionsSet: PropTypes.func.isRequired,
isMobile: PropTypes.bool.isRequired,
optionsDropdownItems: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
type: PropTypes.string,
})).isRequired,
};
const defaultProps = {
@ -128,14 +133,14 @@ const BBB_TABLET_APP_CONFIG = Meteor.settings.public.app.bbbTabletApp;
const { isSafari, isTabletApp } = browserInfo;
const FULLSCREEN_CHANGE_EVENT = isSafari ? 'webkitfullscreenchange' : 'fullscreenchange';
class SettingsDropdown extends PureComponent {
class OptionsDropdown extends PureComponent {
constructor(props) {
super(props);
this.state = {
isAboutModalOpen: false,
isShortcutHelpModalOpen: false,
isSettingsMenuModalOpen: false,
isOptionsMenuModalOpen: false,
isEndMeetingConfirmationModalOpen: false,
isMobileAppModalOpen:false,
isFullscreen: false,
@ -146,7 +151,7 @@ class SettingsDropdown extends PureComponent {
this.leaveSession = this.leaveSession.bind(this);
this.onFullscreenChange = this.onFullscreenChange.bind(this);
this.setSettingsMenuModalIsOpen = this.setSettingsMenuModalIsOpen.bind(this);
this.setOptionsMenuModalIsOpen = this.setOptionsMenuModalIsOpen.bind(this);
this.setEndMeetingConfirmationModalIsOpen = this.setEndMeetingConfirmationModalIsOpen.bind(this);
this.setMobileAppModalIsOpen = this.setMobileAppModalIsOpen.bind(this);
this.setAboutModalIsOpen = this.setAboutModalIsOpen.bind(this);
@ -217,8 +222,8 @@ class SettingsDropdown extends PureComponent {
this.setState({isShortcutHelpModalOpen: value})
}
setSettingsMenuModalIsOpen(value) {
this.setState({isSettingsMenuModalOpen: value})
setOptionsMenuModalIsOpen(value) {
this.setState({isOptionsMenuModalOpen: value})
}
setEndMeetingConfirmationModalIsOpen(value) {
@ -232,7 +237,7 @@ class SettingsDropdown extends PureComponent {
renderMenuItems() {
const {
intl, amIModerator, isBreakoutRoom, isMeteorConnected, audioCaptionsEnabled,
audioCaptionsActive, audioCaptionsSet, isMobile,
audioCaptionsActive, audioCaptionsSet, isMobile, optionsDropdownItems,
} = this.props;
const { isIos } = deviceInfo;
@ -256,7 +261,7 @@ class SettingsDropdown extends PureComponent {
dataTest: 'settings',
label: intl.formatMessage(intlMessages.settingsLabel),
description: intl.formatMessage(intlMessages.settingsDesc),
onClick: () => this.setSettingsMenuModalIsOpen(true),
onClick: () => this.setOptionsMenuModalIsOpen(true),
},
{
key: 'list-item-about',
@ -324,6 +329,27 @@ class SettingsDropdown extends PureComponent {
},
);
optionsDropdownItems.forEach((item) => {
switch (item.type) {
case PluginSdk.OptionsDropdownItemType.OPTION:
this.menuItems.push({
key: item.id,
icon: item.icon,
onClick: item.onClick,
label: item.label,
});
break;
case PluginSdk.OptionsDropdownItemType.SEPARATOR:
this.menuItems.push({
key: item.id,
isSeparator: true,
});
break;
default:
break;
}
});
if (allowLogoutSetting && isMeteorConnected) {
this.menuItems.push(
{
@ -376,7 +402,7 @@ class SettingsDropdown extends PureComponent {
isRTL,
} = this.props;
const { isAboutModalOpen, isShortcutHelpModalOpen, isSettingsMenuModalOpen,
const { isAboutModalOpen, isShortcutHelpModalOpen, isOptionsMenuModalOpen,
isEndMeetingConfirmationModalOpen, isMobileAppModalOpen, } = this.state;
const customStyles = { top: '1rem' };
@ -417,8 +443,8 @@ class SettingsDropdown extends PureComponent {
AboutContainer)}
{this.renderModal(isShortcutHelpModalOpen, this.setShortcutHelpModalIsOpen,
"low", ShortcutHelpComponent)}
{this.renderModal(isSettingsMenuModalOpen, this.setSettingsMenuModalIsOpen,
"low", SettingsMenuContainer)}
{this.renderModal(isOptionsMenuModalOpen, this.setOptionsMenuModalIsOpen,
"low", OptionsMenuContainer)}
{this.renderModal(isEndMeetingConfirmationModalOpen, this.setEndMeetingConfirmationModalIsOpen,
"low", EndMeetingConfirmationContainer)}
{this.renderModal(isMobileAppModalOpen, this.setMobileAppModalIsOpen, "low",
@ -427,6 +453,6 @@ class SettingsDropdown extends PureComponent {
);
}
}
SettingsDropdown.propTypes = propTypes;
SettingsDropdown.defaultProps = defaultProps;
export default withShortcutHelper(injectIntl(SettingsDropdown), 'openOptions');
OptionsDropdown.propTypes = propTypes;
OptionsDropdown.defaultProps = defaultProps;
export default withShortcutHelper(injectIntl(OptionsDropdown), 'openOptions');

View File

@ -1,26 +1,38 @@
import React from 'react';
import { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import deviceInfo from '/imports/utils/deviceInfo';
import browserInfo from '/imports/utils/browserInfo';
import SettingsDropdown from './component';
import OptionsDropdown from './component';
import audioCaptionsService from '/imports/ui/components/audio/captions/service';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
import { layoutSelectInput, layoutSelect } from '../../layout/context';
import { SMALL_VIEWPORT_BREAKPOINT } from '../../layout/enums';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
const { isIphone } = deviceInfo;
const { isSafari, isValidSafariVersion } = browserInfo;
const noIOSFullscreen = !!(((isSafari && !isValidSafariVersion) || isIphone));
const SettingsDropdownContainer = (props) => {
const OptionsDropdownContainer = (props) => {
const { width: browserWidth } = layoutSelectInput((i) => i.browser);
const isMobile = browserWidth <= SMALL_VIEWPORT_BREAKPOINT;
const isRTL = layoutSelect((i) => i.isRTL);
const { pluginsProvidedAggregatedState } = useContext(PluginsContext);
let optionsDropdownItems = [];
if (pluginsProvidedAggregatedState.optionsDropdownItems) {
optionsDropdownItems = [
...pluginsProvidedAggregatedState.optionsDropdownItems,
];
}
return (
<SettingsDropdown {...{ isMobile, isRTL, ...props }} />
<OptionsDropdown {...{
isMobile, isRTL, optionsDropdownItems, ...props,
}}
/>
);
};
@ -38,4 +50,4 @@ export default withTracker((props) => {
isBreakoutRoom: meetingIsBreakout(),
isDropdownOpen: Session.get('dropdownOpen'),
};
})(SettingsDropdownContainer);
})(OptionsDropdownContainer);

View File

@ -6,6 +6,7 @@ import {
colorDanger,
colorGrayDark,
colorBackground,
colorGray,
} from '/imports/ui/stylesheets/styled-components/palette';
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
import { phoneLandscape, smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
@ -79,6 +80,28 @@ const PresentationTitle = styled.h1`
}
`;
const PluginInfoComponent = styled.h1`
font-weight: 400;
color: ${colorWhite};
font-size: ${fontSizeBase};
margin: 0;
padding: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 30vw;
`;
const PluginComponentWrapper = styled.div`
margin: 0 .5rem;
`;
const PluginSeparatorWrapper = styled.div`
color: ${colorGray};
font-size: ${fontSizeBase};
margin: 0 1rem;
`;
const Right = styled.div`
display: flex;
flex-direction: row;
@ -128,4 +151,7 @@ export default {
Right,
Bottom,
NavbarToggleButton,
PluginInfoComponent,
PluginComponentWrapper,
PluginSeparatorWrapper,
};

View File

@ -27,7 +27,7 @@ const PluginLoaderContainer = (props: PluginLoaderContainerProps) => {
logger.info(`Loaded plugin ${plugin.name}`);
};
script.onerror = (err) => {
logger.info(`Error when loading plugin ${plugin.name}, error: ${err}`);
logger.error(`Error when loading plugin ${plugin.name}, error: `, err);
};
script.src = plugin.url;
script.setAttribute('uuid', div.id);

View File

@ -0,0 +1,54 @@
import { useEffect, useState, useContext } from 'react';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import {
PluginProvidedStateContainerChildProps, PluginProvidedState,
PluginProvidedStateContainerChild,
} from '../../types';
import { PluginsContext } from '../../../components-data/plugin-context/context';
const ActionBarPluginStateContainer = ((
props: PluginProvidedStateContainerChildProps,
) => {
const {
uuid,
generateItemWithId,
pluginProvidedStateMap,
pluginApi,
} = props;
const [
actionBarItems,
setActionBarItems,
] = useState<PluginSdk.ActionsBarItem[]>([]);
const {
pluginsProvidedAggregatedState,
setPluginsProvidedAggregatedState,
} = useContext(PluginsContext);
useEffect(() => {
// Change this plugin provided toolbar items
pluginProvidedStateMap[uuid].actionsBarItems = actionBarItems;
// Update context with computed aggregated list of all plugin provided toolbar items
const aggregatedActionBarItems = (
[] as PluginSdk.ActionsBarItem[]).concat(
...Object.values(pluginProvidedStateMap)
.map((pps: PluginProvidedState) => pps.actionsBarItems),
);
setPluginsProvidedAggregatedState(
{
...pluginsProvidedAggregatedState,
actionsBarItems: aggregatedActionBarItems,
},
);
}, [actionBarItems]);
pluginApi.setActionsBarItems = (items: PluginSdk.ActionsBarItem[]) => {
const itemsWithId = items.map(generateItemWithId) as PluginSdk.ActionsBarItem[];
return setActionBarItems(itemsWithId);
};
return null;
}) as PluginProvidedStateContainerChild;
export default ActionBarPluginStateContainer;

View File

@ -0,0 +1,53 @@
import { useEffect, useState, useContext } from 'react';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import {
PluginProvidedStateContainerChildProps, PluginProvidedState,
PluginProvidedStateContainerChild,
} from '../../types';
import { PluginsContext } from '../../../components-data/plugin-context/context';
const AudioSettingsDropdownPluginStateContainer = ((
props: PluginProvidedStateContainerChildProps,
) => {
const {
uuid,
generateItemWithId,
pluginProvidedStateMap,
pluginApi,
} = props;
const [
audioSettingsDropdownItems,
setAudioSettingsDropdownItems,
] = useState<PluginSdk.AudioSettingsDropdownItem[]>([]);
const {
pluginsProvidedAggregatedState,
setPluginsProvidedAggregatedState,
} = useContext(PluginsContext);
useEffect(() => {
// Change this plugin provided toolbar items
pluginProvidedStateMap[uuid].audioSettingsDropdownItems = audioSettingsDropdownItems;
// Update context with computed aggregated list of all plugin provided toolbar items
const aggregatedAudioSettingsDropdownItems = ([] as PluginSdk.AudioSettingsDropdownItem[]).concat(
...Object.values(pluginProvidedStateMap)
.map((pps: PluginProvidedState) => pps.audioSettingsDropdownItems),
);
setPluginsProvidedAggregatedState(
{
...pluginsProvidedAggregatedState,
audioSettingsDropdownItems: aggregatedAudioSettingsDropdownItems,
},
);
}, [audioSettingsDropdownItems]);
pluginApi.setAudioSettingsDropdownItems = (items: PluginSdk.AudioSettingsDropdownItem[]) => {
const itemsWithId = items.map(generateItemWithId) as PluginSdk.AudioSettingsDropdownItem[];
return setAudioSettingsDropdownItems(itemsWithId);
};
return null;
}) as PluginProvidedStateContainerChild;
export default AudioSettingsDropdownPluginStateContainer;

View File

@ -0,0 +1,54 @@
import { useEffect, useState, useContext } from 'react';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import {
PluginProvidedStateContainerChildProps, PluginProvidedState,
PluginProvidedStateContainerChild,
} from '../../types';
import { PluginsContext } from '../../../components-data/plugin-context/context';
const CameraSettingsDropdownPluginStateContainer = ((
props: PluginProvidedStateContainerChildProps,
) => {
const {
uuid,
generateItemWithId,
pluginProvidedStateMap,
pluginApi,
} = props;
const [
cameraSettingsDropdownItems,
setCameraSettingsDropdownItems,
] = useState<PluginSdk.CameraSettingsDropdownItem[]>([]);
const {
pluginsProvidedAggregatedState,
setPluginsProvidedAggregatedState,
} = useContext(PluginsContext);
useEffect(() => {
// Change this plugin provided toolbar items
pluginProvidedStateMap[uuid].cameraSettingsDropdownItems = cameraSettingsDropdownItems;
// Update context with computed aggregated list of all plugin provided toolbar items
const aggregatedCameraSettingsDropdownItems = (
[] as PluginSdk.CameraSettingsDropdownItem[]).concat(
...Object.values(pluginProvidedStateMap)
.map((pps: PluginProvidedState) => pps.cameraSettingsDropdownItems),
);
setPluginsProvidedAggregatedState(
{
...pluginsProvidedAggregatedState,
cameraSettingsDropdownItems: aggregatedCameraSettingsDropdownItems,
},
);
}, [cameraSettingsDropdownItems]);
pluginApi.setCameraSettingsDropdownItems = (items: PluginSdk.CameraSettingsDropdownItem[]) => {
const itemsWithId = items.map(generateItemWithId) as PluginSdk.CameraSettingsDropdownItem[];
return setCameraSettingsDropdownItems(itemsWithId);
};
return null;
}) as PluginProvidedStateContainerChild;
export default CameraSettingsDropdownPluginStateContainer;

View File

@ -8,6 +8,12 @@ import {
import PresentationToolbarPluginStateContainer from './presentation-toolbar/container';
import UserListDropdownPluginStateContainer from './user-list-dropdown/container';
import ActionButtonDropdownPluginStateContainer from './action-button-dropdown/container';
import AudioSettingsDropdownPluginStateContainer from './audio-settings-dropdown/container';
import ActionBarPluginStateContainer from './action-bar/container';
import PresentationDropdownPluginStateContainer from './presentation-dropdown/container';
import NavBarPluginStateContainer from './nav-bar/container';
import OptionsDropdownPluginStateContainer from './options-dropdown/container';
import CameraSettingsDropdownPluginStateContainer from './camera-settings-dropdown/container';
import UserCameraDropdownPluginStateContainer from './user-camera-dropdown/container';
const pluginProvidedStateMap: PluginsProvidedStateMap = {};
@ -16,6 +22,12 @@ const pluginProvidedStateContainers: PluginProvidedStateContainerChild[] = [
PresentationToolbarPluginStateContainer,
UserListDropdownPluginStateContainer,
ActionButtonDropdownPluginStateContainer,
AudioSettingsDropdownPluginStateContainer,
ActionBarPluginStateContainer,
PresentationDropdownPluginStateContainer,
NavBarPluginStateContainer,
OptionsDropdownPluginStateContainer,
CameraSettingsDropdownPluginStateContainer,
UserCameraDropdownPluginStateContainer,
];

View File

@ -0,0 +1,54 @@
import { useEffect, useState, useContext } from 'react';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import {
PluginProvidedStateContainerChildProps, PluginProvidedState,
PluginProvidedStateContainerChild,
} from '../../types';
import { PluginsContext } from '../../../components-data/plugin-context/context';
const NavBarPluginStateContainer = ((
props: PluginProvidedStateContainerChildProps,
) => {
const {
uuid,
generateItemWithId,
pluginProvidedStateMap,
pluginApi,
} = props;
const [
navBarItems,
setNavBarItems,
] = useState<PluginSdk.NavBarItem[]>([]);
const {
pluginsProvidedAggregatedState,
setPluginsProvidedAggregatedState,
} = useContext(PluginsContext);
useEffect(() => {
// Change this plugin provided toolbar items
pluginProvidedStateMap[uuid].navBarItems = navBarItems;
// Update context with computed aggregated list of all plugin provided toolbar items
const aggregatedNavBarItems = ([] as PluginSdk.NavBarItem[]).concat(
...Object.values(pluginProvidedStateMap)
.map((pps: PluginProvidedState) => pps.navBarItems),
);
setPluginsProvidedAggregatedState(
{
...pluginsProvidedAggregatedState,
navBarItems: aggregatedNavBarItems,
},
);
}, [navBarItems]);
pluginApi.setNavBarItems = (items: PluginSdk.NavBarItem[]) => {
const itemsWithId = items.map(generateItemWithId) as PluginSdk.NavBarItem[];
return setNavBarItems(itemsWithId);
};
return null;
}) as PluginProvidedStateContainerChild;
export default NavBarPluginStateContainer;

View File

@ -0,0 +1,54 @@
import { useEffect, useState, useContext } from 'react';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import {
PluginProvidedStateContainerChildProps, PluginProvidedState,
PluginProvidedStateContainerChild,
} from '../../types';
import { PluginsContext } from '../../../components-data/plugin-context/context';
const OptionsDropdownPluginStateContainer = ((
props: PluginProvidedStateContainerChildProps,
) => {
const {
uuid,
generateItemWithId,
pluginProvidedStateMap,
pluginApi,
} = props;
const [
optionsDropdownItems,
setOptionsDropdownItems,
] = useState<PluginSdk.OptionsDropdownItem[]>([]);
const {
pluginsProvidedAggregatedState,
setPluginsProvidedAggregatedState,
} = useContext(PluginsContext);
useEffect(() => {
// Change this plugin provided toolbar items
pluginProvidedStateMap[uuid].optionsDropdownItems = optionsDropdownItems;
// Update context with computed aggregated list of all plugin provided toolbar items
const aggregatedOptionsDropdownItems = (
[] as PluginSdk.OptionsDropdownItem[]).concat(
...Object.values(pluginProvidedStateMap)
.map((pps: PluginProvidedState) => pps.optionsDropdownItems),
);
setPluginsProvidedAggregatedState(
{
...pluginsProvidedAggregatedState,
optionsDropdownItems: aggregatedOptionsDropdownItems,
},
);
}, [optionsDropdownItems]);
pluginApi.setOptionsDropdownItems = (items: PluginSdk.OptionsDropdownItem[]) => {
const itemsWithId = items.map(generateItemWithId) as PluginSdk.OptionsDropdownItem[];
return setOptionsDropdownItems(itemsWithId);
};
return null;
}) as PluginProvidedStateContainerChild;
export default OptionsDropdownPluginStateContainer;

View File

@ -0,0 +1,54 @@
import { useEffect, useState, useContext } from 'react';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import {
PluginProvidedStateContainerChildProps, PluginProvidedState,
PluginProvidedStateContainerChild,
} from '../../types';
import { PluginsContext } from '../../../components-data/plugin-context/context';
const PresentationDropdownPluginStateContainer = ((
props: PluginProvidedStateContainerChildProps,
) => {
const {
uuid,
generateItemWithId,
pluginProvidedStateMap,
pluginApi,
} = props;
const [
presentationDropdownItems,
setPresentationDropdownItems,
] = useState<PluginSdk.PresentationDropdownItem[]>([]);
const {
pluginsProvidedAggregatedState,
setPluginsProvidedAggregatedState,
} = useContext(PluginsContext);
useEffect(() => {
// Change this plugin provided toolbar items
pluginProvidedStateMap[uuid].presentationDropdownItems = presentationDropdownItems;
// Update context with computed aggregated list of all plugin provided toolbar items
const aggregatedPresentationDropdownItems = (
[] as PluginSdk.PresentationDropdownItem[]).concat(
...Object.values(pluginProvidedStateMap)
.map((pps: PluginProvidedState) => pps.presentationDropdownItems),
);
setPluginsProvidedAggregatedState(
{
...pluginsProvidedAggregatedState,
presentationDropdownItems: aggregatedPresentationDropdownItems,
},
);
}, [presentationDropdownItems]);
pluginApi.setPresentationDropdownItems = (items: PluginSdk.PresentationDropdownItem[]) => {
const itemsWithId = items.map(generateItemWithId) as PluginSdk.PresentationDropdownItem[];
return setPresentationDropdownItems(itemsWithId);
};
return null;
}) as PluginProvidedStateContainerChild;
export default PresentationDropdownPluginStateContainer;

View File

@ -30,6 +30,12 @@ export interface PluginProvidedState {
presentationToolbarItems: PluginSdk.PresentationToolbarItem[];
userListDropdownItems: PluginSdk.UserListDropdownItem[];
actionButtonDropdownItems: PluginSdk.ActionButtonDropdownItem[];
audioSettingsDropdownItems: PluginSdk.AudioSettingsDropdownItem[];
actionsBarItems: PluginSdk.ActionsBarItem[];
presentationDropdownItems: PluginSdk.PresentationDropdownItem[];
navBarItems: PluginSdk.NavBarItem[];
optionsDropdownItems: PluginSdk.OptionsDropdownItem[];
cameraSettingsDropdownItems: PluginSdk.CameraSettingsDropdownItem[];
userCameraDropdownItems: PluginSdk.UserCameraDropdownItem[];
}

View File

@ -10,6 +10,7 @@ import TooltipContainer from '/imports/ui/components/common/tooltip/container';
import { ACTIONS } from '/imports/ui/components/layout/enums';
import browserInfo from '/imports/utils/browserInfo';
import AppService from '/imports/ui/components/app/service';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
const intlMessages = defineMessages({
downloading: {
@ -43,7 +44,7 @@ const intlMessages = defineMessages({
defaultMessage: 'Minimize',
},
optionsLabel: {
id: 'app.navBar.settingsDropdown.optionsLabel',
id: 'app.navBar.optionsDropdown.optionsLabel',
description: 'Options button label',
defaultMessage: 'Options',
},
@ -88,6 +89,10 @@ const propTypes = {
getShapes: PropTypes.func.isRequired,
currentPageId: PropTypes.string.isRequired,
}),
presentationDropdownItems: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
type: PropTypes.string,
})).isRequired,
};
const defaultProps = {
@ -124,6 +129,7 @@ const PresentationMenu = (props) => {
isToolbarVisible,
setIsToolbarVisible,
allowSnapshotOfCurrentSlide,
presentationDropdownItems,
} = props;
const [state, setState] = useState({
@ -298,6 +304,27 @@ const PresentationMenu = (props) => {
);
}
presentationDropdownItems.forEach((item, index) => {
switch (item.type) {
case PluginSdk.PresentationDropdownItemType.OPTION:
menuItems.push({
key: `${item.id}-${index}`,
label: item.label,
icon: item.icon,
onClick: item.onClick,
});
break;
case PluginSdk.PresentationDropdownItemType.SEPARATOR:
menuItems.push({
key: `${item.id}-${index}`,
isSeparator: true,
});
break;
default:
break;
}
});
return menuItems;
}

View File

@ -1,4 +1,5 @@
import React from 'react';
import { useContext } from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import PresentationMenu from './component';
@ -9,6 +10,7 @@ import { layoutSelect, layoutDispatch } from '/imports/ui/components/layout/cont
import WhiteboardService from '/imports/ui/components/whiteboard/service';
import UserService from '/imports/ui/components/user-list/service';
import { isSnapshotOfCurrentSlideEnabled } from '/imports/ui/services/features';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
const PresentationMenuContainer = (props) => {
const fullscreen = layoutSelect((i) => i.fullscreen);
@ -17,6 +19,13 @@ const PresentationMenuContainer = (props) => {
const { elementId } = props;
const isFullscreen = currentElement === elementId;
const isRTL = layoutSelect((i) => i.isRTL);
const { pluginsProvidedAggregatedState } = useContext(PluginsContext);
let presentationDropdownItems = [];
if (pluginsProvidedAggregatedState.presentationDropdownItems) {
presentationDropdownItems = [
...pluginsProvidedAggregatedState.presentationDropdownItems,
];
}
return (
<PresentationMenu
@ -27,6 +36,7 @@ const PresentationMenuContainer = (props) => {
isFullscreen,
layoutContextDispatch,
isRTL,
presentationDropdownItems,
}}
/>
);

View File

@ -10,6 +10,7 @@ import BBBMenu from '/imports/ui/components/common/menu/component';
import { isVirtualBackgroundsEnabled } from '/imports/ui/services/features';
import Button from '/imports/ui/components/common/button/component';
import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import Settings from '/imports/ui/services/settings';
const ENABLE_WEBCAM_SELECTOR_BUTTON = Meteor.settings.public.app.enableWebcamSelectorButton;
@ -60,6 +61,10 @@ const propTypes = {
intl: PropTypes.object.isRequired,
hasVideoStream: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
cameraSettingsDropdownItems: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
type: PropTypes.string,
})).isRequired,
};
const JoinVideoButton = ({
@ -68,6 +73,7 @@ const JoinVideoButton = ({
status,
disableReason,
updateSettings,
cameraSettingsDropdownItems,
}) => {
const { isMobile } = deviceInfo;
const isMobileSharingCamera = hasVideoStream && isMobile;
@ -163,6 +169,26 @@ const JoinVideoButton = ({
if (actions.length === 0) return null;
const customStyles = { top: '-3.6rem' };
cameraSettingsDropdownItems.forEach((plugin) => {
switch (plugin.type) {
case PluginSdk.CameraSettingsDropdownItemType.OPTION:
actions.push({
key: plugin.id,
label: plugin.label,
onClick: plugin.onClick,
icon: plugin.icon,
});
break;
case PluginSdk.CameraSettingsDropdownItemType.SEPARATOR:
actions.push({
key: plugin.id,
isSeparator: true,
});
break;
default:
break;
}
});
return (
<BBBMenu
customStyles={!isMobile ? customStyles : null}

View File

@ -1,4 +1,5 @@
import React from 'react';
import { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { injectIntl } from 'react-intl';
import JoinVideoButton from './component';
@ -6,6 +7,7 @@ import VideoService from '../service';
import {
updateSettings,
} from '/imports/ui/components/settings/service';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
const JoinVideoOptionsContainer = (props) => {
const {
@ -17,9 +19,23 @@ const JoinVideoOptionsContainer = (props) => {
...restProps
} = props;
const {
pluginsProvidedAggregatedState,
} = useContext(PluginsContext);
let cameraSettingsDropdownItems = [];
if (pluginsProvidedAggregatedState.cameraSettingsDropdownItems) {
cameraSettingsDropdownItems = [
...pluginsProvidedAggregatedState.cameraSettingsDropdownItems,
];
}
return (
<JoinVideoButton {...{
hasVideoStream, updateSettings, disableReason, status, ...restProps,
cameraSettingsDropdownItems,
hasVideoStream,
updateSettings,
disableReason,
status,
...restProps,
}}
/>
);

View File

@ -3754,9 +3754,9 @@
"dev": true
},
"bigbluebutton-html-plugin-sdk": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.7.tgz",
"integrity": "sha512-9bCJwVGJoEhhwCNP/N1d/loSLsK6DgWsc6zcwqPk6mOzSlNeiJ0+uzhg4sB44JYN2vr6S7+BrhPF5rBwrT3ExA=="
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.14.tgz",
"integrity": "sha512-Yz1+E36U24949zjmevT9Kthg1d4iog5k1M2qPHs/ovIbTed+T24StmmxC8AX/8ZmwKUSCZvWmT5gM7qOXN1X+w=="
},
"bintrees": {
"version": "1.0.2",

View File

@ -45,7 +45,7 @@
"autoprefixer": "^10.4.4",
"axios": "^0.21.3",
"babel-runtime": "~6.26.0",
"bigbluebutton-html-plugin-sdk": "0.0.7",
"bigbluebutton-html-plugin-sdk": "0.0.14",
"bowser": "^2.11.0",
"browser-bunyan": "^1.8.0",
"classnames": "^2.2.6",

View File

@ -430,24 +430,24 @@
"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.optionsDropdown.optionsLabel": "Options",
"app.navBar.optionsDropdown.fullscreenLabel": "Fullscreen Application",
"app.navBar.optionsDropdown.settingsLabel": "Settings",
"app.navBar.optionsDropdown.aboutLabel": "About",
"app.navBar.optionsDropdown.leaveSessionLabel": "Leave meeting",
"app.navBar.optionsDropdown.exitFullscreenLabel": "Exit Fullscreen",
"app.navBar.optionsDropdown.fullscreenDesc": "Make the settings menu fullscreen",
"app.navBar.optionsDropdown.settingsDesc": "Change the general settings",
"app.navBar.optionsDropdown.aboutDesc": "Show information about the client",
"app.navBar.optionsDropdown.leaveSessionDesc": "Leave the meeting",
"app.navBar.optionsDropdown.exitFullscreenDesc": "Exit fullscreen mode",
"app.navBar.optionsDropdown.hotkeysLabel": "Keyboard shortcuts",
"app.navBar.optionsDropdown.hotkeysDesc": "Listing of available keyboard shortcuts",
"app.navBar.optionsDropdown.helpLabel": "Help",
"app.navBar.optionsDropdown.openAppLabel": "Open in BigBlueButton Tablet app",
"app.navBar.optionsDropdown.helpDesc": "Links user to video tutorials (opens new tab)",
"app.navBar.optionsDropdown.endMeetingDesc": "Terminates the current meeting",
"app.navBar.optionsDropdown.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",