Merge pull request #19177 from Scroody/issue-12614

feat: leave meeting button
This commit is contained in:
Ramón Souza 2024-02-23 14:13:01 -03:00 committed by GitHub
commit a813624f01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 305 additions and 22 deletions

View File

@ -65,6 +65,7 @@ const currentParameters = [
'bbb_hide_actions_bar',
'bbb_hide_nav_bar',
'bbb_change_layout',
'bbb_direct_leave_button',
];
function valueParser(val) {

View File

@ -193,6 +193,7 @@ class ActionsDropdown extends PureComponent {
isCameraAsContentEnabled,
isTimerFeatureEnabled,
presentations,
isDirectLeaveButtonEnabled,
} = this.props;
const { pollBtnLabel, presentationLabel, takePresenter } = intlMessages;
@ -295,6 +296,7 @@ class ActionsDropdown extends PureComponent {
key: 'layoutModal',
onClick: () => this.setLayoutModalIsOpen(true),
dataTest: 'manageLayoutBtn',
divider: !isDirectLeaveButtonEnabled,
});
}

View File

@ -42,6 +42,7 @@ import {
btnDangerBorder,
btnDangerColor,
btnDangerBg,
btnDangerBgHover,
btnDarkBorder,
btnDarkColor,
btnDarkBg,
@ -522,6 +523,7 @@ const ButtonSpan = styled.span`
&:hover,
.buttonWrapper:hover & {
color: ${btnDangerColor};
background-color: ${btnDangerBgHover};
}
`}

View File

@ -18,6 +18,7 @@ import deviceInfo from '/imports/utils/deviceInfo';
import { PANELS, ACTIONS, LAYOUT_TYPE } from '../layout/enums';
import Button from '/imports/ui/components/common/button/component';
import { isEqual } from 'radash';
import LeaveMeetingButtonContainer from './leave-meeting-button/container';
import Settings from '/imports/ui/services/settings';
const intlMessages = defineMessages({
@ -41,6 +42,10 @@ const intlMessages = defineMessages({
id: 'app.createBreakoutRoom.room',
description: 'default breakout room name',
},
leaveMeetingLabel: {
id: 'app.navBar.leaveMeetingBtnLabel',
description: 'Leave meeting button label',
},
});
const propTypes = {
@ -277,6 +282,8 @@ class NavBar extends Component {
isPinned,
sidebarNavigation,
currentUserId,
isDirectLeaveButtonEnabled,
isMeteorConnected,
} = this.props;
const hasNotification = hasUnreadMessages || (hasUnreadNotes && !isPinned);
@ -364,8 +371,12 @@ class NavBar extends Component {
<Styled.Right>
{renderPluginItems(rightPluginItems)}
{ConnectionStatusService.isEnabled() ? <ConnectionStatusButton /> : null}
{ConnectionStatusService.isEnabled() ? <ConnectionStatus /> : null}
<OptionsDropdownContainer amIModerator={amIModerator} />
{isDirectLeaveButtonEnabled && isMeteorConnected
? <LeaveMeetingButtonContainer amIModerator={amIModerator} /> : null}
<OptionsDropdownContainer
amIModerator={amIModerator}
isDirectLeaveButtonEnabled={isDirectLeaveButtonEnabled}
/>
</Styled.Right>
</Styled.Top>
<Styled.Bottom>

View File

@ -119,6 +119,11 @@ export default withTracker(() => {
}
}
const IS_DIRECT_LEAVE_BUTTON_ENABLED = getFromUserSettings(
'bbb_direct_leave_button',
PUBLIC_CONFIG.app.defaultSettings.application.directLeaveButton,
);
return {
isPinned: NotesService.isSharedNotesPinned(),
currentUserId: Auth.userID,
@ -128,5 +133,7 @@ export default withTracker(() => {
breakoutName,
meetingName,
unread,
isDirectLeaveButtonEnabled: IS_DIRECT_LEAVE_BUTTON_ENABLED,
isMeteorConnected: Meteor.status().connected,
};
})(NavBarContainer);

View File

@ -0,0 +1,198 @@
import React, { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import EndMeetingConfirmationContainer from '/imports/ui/components/end-meeting-confirmation/container';
import { makeCall } from '/imports/ui/services/api';
import BBBMenu from '/imports/ui/components/common/menu/component';
import { colorDanger, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import Styled from './styles';
const intlMessages = defineMessages({
leaveMeetingBtnLabel: {
id: 'app.navBar.leaveMeetingBtnLabel',
description: 'Leave meeting button label',
},
leaveMeetingBtnDesc: {
id: 'app.navBar.leaveMeetingBtnDesc',
description: 'Describes the leave meeting button',
},
leaveSessionLabel: {
id: 'app.navBar.optionsDropdown.leaveSessionLabel',
description: 'Leave session button label',
},
leaveSessionDesc: {
id: 'app.navBar.settingsDropdown.leaveSessionDesc',
description: 'Describes leave session option',
},
endMeetingLabel: {
id: 'app.navBar.optionsDropdown.endMeetingForAllLabel',
description: 'End meeting button label',
},
endMeetingDesc: {
id: 'app.navBar.settingsDropdown.endMeetingDesc',
description: 'Describes settings option closing the current meeting',
},
});
const propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
amIModerator: PropTypes.bool,
isBreakoutRoom: PropTypes.bool,
isMeteorConnected: PropTypes.bool.isRequired,
isDropdownOpen: PropTypes.bool,
isMobile: PropTypes.bool.isRequired,
};
const defaultProps = {
amIModerator: false,
isBreakoutRoom: false,
isDropdownOpen: false,
};
class LeaveMeetingButton extends PureComponent {
constructor(props) {
super(props);
this.state = {
isEndMeetingConfirmationModalOpen: false,
};
// Set the logout code to 680 because it's not a real code and can be matched on the other side
this.LOGOUT_CODE = '680';
this.setEndMeetingConfirmationModalIsOpen = this
.setEndMeetingConfirmationModalIsOpen.bind(this);
this.leaveSession = this.leaveSession.bind(this);
}
setEndMeetingConfirmationModalIsOpen(value) {
this.setState({ isEndMeetingConfirmationModalOpen: value });
}
leaveSession() {
makeCall('userLeftMeeting');
// we don't check askForFeedbackOnLogout here,
// it is checked in meeting-ended component
Session.set('codeError', this.LOGOUT_CODE);
}
renderMenuItems() {
const {
intl, amIModerator, isBreakoutRoom, isMeteorConnected,
} = this.props;
const allowedToEndMeeting = amIModerator && !isBreakoutRoom;
const { allowLogout: allowLogoutSetting } = Meteor.settings.public.app;
this.menuItems = [];
if (allowLogoutSetting && isMeteorConnected) {
this.menuItems.push(
{
key: 'list-item-logout',
dataTest: 'logoutButton',
icon: 'logout',
label: intl.formatMessage(intlMessages.leaveSessionLabel),
description: intl.formatMessage(intlMessages.leaveSessionDesc),
onClick: () => this.leaveSession(),
},
);
}
if (allowedToEndMeeting && isMeteorConnected) {
const customStyles = { background: colorDanger, color: colorWhite };
this.menuItems.push(
{
key: 'list-item-end-meeting',
dataTest: 'endMeetingButton',
icon: 'close',
label: intl.formatMessage(intlMessages.endMeetingLabel),
description: intl.formatMessage(intlMessages.endMeetingDesc),
customStyles,
onClick: () => this.setEndMeetingConfirmationModalIsOpen(true),
},
);
}
return this.menuItems;
}
// eslint-disable-next-line class-methods-use-this
renderModal(
isOpen,
setIsOpen,
priority,
Component,
otherOptions,
) {
return isOpen ? (
<Component
{...{
...otherOptions,
onRequestClose: () => setIsOpen(false),
priority,
setIsOpen,
isOpen,
}}
/>
) : null;
}
render() {
const {
intl,
isDropdownOpen,
isMobile,
isRTL,
} = this.props;
const { isEndMeetingConfirmationModalOpen } = this.state;
const customStyles = { top: '1rem' };
return (
<>
<BBBMenu
customStyles={!isMobile ? customStyles : null}
trigger={(
<Styled.LeaveButton
state={isDropdownOpen ? 'open' : 'closed'}
aria-label={intl.formatMessage(intlMessages.leaveMeetingBtnLabel)}
tooltipLabel={intl.formatMessage(intlMessages.leaveMeetingBtnLabel)}
description={intl.formatMessage(intlMessages.leaveMeetingBtnDesc)}
data-test="leaveMeetingDropdown"
icon="logout"
color="danger"
size="lg"
// FIXME: Without onClick react proptypes keep warning
// even after the DropdownTrigger inject an onClick handler
onClick={() => null}
/>
)}
actions={this.renderMenuItems()}
opts={{
id: 'app-leave-meeting-menu',
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getcontentanchorel: null,
fullwidth: 'true',
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'left' : 'right' },
transformorigin: { vertical: 'top', horizontal: isRTL ? 'left' : 'right' },
}}
/>
{this.renderModal(isEndMeetingConfirmationModalOpen,
this.setEndMeetingConfirmationModalIsOpen,
'low', EndMeetingConfirmationContainer)}
</>
);
}
}
LeaveMeetingButton.propTypes = propTypes;
LeaveMeetingButton.defaultProps = defaultProps;
export default injectIntl(LeaveMeetingButton);

View File

@ -0,0 +1,25 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import deviceInfo from '/imports/utils/deviceInfo';
import LeaveMeetingButton from './component';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
import { layoutSelectInput, layoutSelect } from '../../layout/context';
import { SMALL_VIEWPORT_BREAKPOINT } from '../../layout/enums';
const LeaveMeetingButtonContainer = (props) => {
const { width: browserWidth } = layoutSelectInput((i) => i.browser);
const isMobile = browserWidth <= SMALL_VIEWPORT_BREAKPOINT;
const isRTL = layoutSelect((i) => i.isRTL);
return (
<LeaveMeetingButton {...{ isMobile, isRTL, ...props }} />
);
};
export default withTracker((props) => ({
amIModerator: props.amIModerator,
isMobile: deviceInfo.isMobile,
isMeteorConnected: Meteor.status().connected,
isBreakoutRoom: meetingIsBreakout(),
isDropdownOpen: Session.get('dropdownOpen'),
}))(LeaveMeetingButtonContainer);

View File

@ -0,0 +1,24 @@
import styled from 'styled-components';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import Button from '/imports/ui/components/common/button/component';
const LeaveButton = styled(Button)`
${({ state }) => state === 'open' && `
@media ${smallOnly} {
display: none;
}
`}
${({ state }) => state === 'closed' && `
margin-left: 1.0rem;
margin-right: 0.5rem;
border-radius: 1.1rem;
font-size: 1rem;
line-height: 1.1rem;
font-weight: 400;
z-index: 3;
`}
`;
export default {
LeaveButton,
};

View File

@ -81,7 +81,7 @@ const intlMessages = defineMessages({
description: 'Describes help option',
},
endMeetingLabel: {
id: 'app.navBar.optionsDropdown.endMeetingLabel',
id: 'app.navBar.optionsDropdown.endMeetingForAllLabel',
description: 'End meeting options label',
},
endMeetingDesc: {
@ -113,6 +113,7 @@ const propTypes = {
audioCaptionsActive: PropTypes.bool.isRequired,
audioCaptionsSet: PropTypes.func.isRequired,
isMobile: PropTypes.bool.isRequired,
isDirectLeaveButtonEnabled: PropTypes.bool.isRequired,
optionsDropdownItems: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
type: PropTypes.string,
@ -143,7 +144,7 @@ class OptionsDropdown extends PureComponent {
isShortcutHelpModalOpen: false,
isOptionsMenuModalOpen: false,
isEndMeetingConfirmationModalOpen: false,
isMobileAppModalOpen:false,
isMobileAppModalOpen: false,
isFullscreen: false,
};
@ -240,7 +241,7 @@ class OptionsDropdown extends PureComponent {
renderMenuItems() {
const {
intl, amIModerator, isBreakoutRoom, isMeteorConnected, audioCaptionsEnabled,
audioCaptionsActive, audioCaptionsSet, isMobile, optionsDropdownItems,
audioCaptionsActive, audioCaptionsSet, isMobile, optionsDropdownItems, isDirectLeaveButtonEnabled,
} = this.props;
const { isIos } = deviceInfo;
@ -353,7 +354,7 @@ class OptionsDropdown extends PureComponent {
}
});
if (allowLogoutSetting && isMeteorConnected) {
if (allowLogoutSetting && isMeteorConnected && !isDirectLeaveButtonEnabled) {
this.menuItems.push(
{
key: 'list-item-logout',
@ -366,13 +367,13 @@ class OptionsDropdown extends PureComponent {
);
}
if (allowedToEndMeeting && isMeteorConnected) {
if (allowedToEndMeeting && isMeteorConnected && !isDirectLeaveButtonEnabled) {
const customStyles = { background: colorDanger, color: colorWhite };
this.menuItems.push(
{
key: 'list-item-end-meeting',
icon: 'application',
icon: 'close',
label: intl.formatMessage(intlMessages.endMeetingLabel),
description: intl.formatMessage(intlMessages.endMeetingDesc),
customStyles,

View File

@ -64,6 +64,7 @@ const btnWarningBg = `var(--btn-warning-bg, ${colorWarning})`;
const btnDangerBorder = `var(--btn-danger-border, ${colorDanger})`;
const btnDangerColor = `var(--btn-danger-color, ${colorWhite})`;
const btnDangerBg = `var(--btn-danger-bg, ${colorDanger})`;
const btnDangerBgHover = 'var(--btn-danger-bg-hover, #C61C1C)';
const btnDarkBorder = `var(--btn-dark-border, ${colorDanger})`;
const btnDarkColor = `var(--btn-dark-color, ${colorWhite})`;
@ -184,6 +185,7 @@ export {
userThumbnailBorder,
loaderBg,
loaderBullet,
btnDangerBgHover,
systemMessageBackgroundColor,
systemMessageBorderColor,
systemMessageFontColor,

View File

@ -58,7 +58,7 @@ public:
askForFeedbackOnLogout: false
# the default logoutUrl matches window.location.origin i.e. bigbluebutton.org for demo.bigbluebutton.org
# in some cases we want only custom logoutUrl to be used when provided on meeting create. Default value: true
askForConfirmationOnLeave: true
askForConfirmationOnLeave: false
wakeLock:
enabled: true
allowDefaultLogoutUrl: true
@ -200,6 +200,7 @@ public:
paginationEnabled: true
whiteboardToolbarAutoHide: false
autoCloseReactionsBar: true
directLeaveButton: true
darkTheme: false
# fallbackLocale: if the locale the client is loaded in does not have a
# translation a string, it will use the translation from the locale

View File

@ -437,6 +437,7 @@
"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.leaveMeetingBtnLabel": "Leave",
"app.navBar.optionsDropdown.optionsLabel": "Options",
"app.navBar.optionsDropdown.fullscreenLabel": "Fullscreen Application",
"app.navBar.optionsDropdown.settingsLabel": "Settings",
@ -455,6 +456,7 @@
"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.optionsDropdown.endMeetingForAllLabel": "End meeting for all",
"app.navBar.userListToggleBtnLabel": "User list toggle",
"app.navBar.toggleUserList.ariaLabel": "Users and messages toggle",
"app.navBar.toggleUserList.newMessages": "with new message notification",

View File

@ -381,8 +381,7 @@ class Chat extends MultiUsers {
async chatDisabledUserLeaves() {
await openPrivateChat(this.modPage);
await this.modPage.waitForSelector(e.sendButton);
await this.userPage.waitAndClick(e.optionsButton);
await this.userPage.waitAndClick(e.logout);
await this.userPage.logoutFromMeeting();
await this.modPage.hasElement(e.partnerDisconnectedMessage, ELEMENT_WAIT_LONGER_TIME);
await this.modPage.wasRemoved(e.sendButton);
}

View File

@ -16,7 +16,8 @@ exports.raiseHandBtn = 'div[data-test="raiseHandBtn"]';
exports.lowerHandBtn = 'div[data-test="lowerHandBtn"]';
exports.raiseHandRejection = 'button[data-test="raiseHandRejection"]';
exports.meetingEndedModal = 'div[data-test="meetingEndedModal"]';
exports.logout = 'li[data-test="logout"]';
exports.leaveMeetingDropdown = 'button[data-test="leaveMeetingDropdown"]';
exports.logoutBtn = 'li[data-test="logoutButton"]';
exports.rating = 'div[data-test="rating"]';
exports.errorScreenMessage = 'h1[data-test="errorScreenMessage"]';
exports.errorMessageLabel = 'span[id="error-message"]';

View File

@ -88,12 +88,18 @@ class Page {
}
async logoutFromMeeting() {
await this.waitAndClick(e.optionsButton);
await this.waitAndClick(e.logout);
const { directLeaveButton } = this.settings;
if (directLeaveButton) {
await this.waitAndClick(e.leaveMeetingDropdown);
} else {
await this.waitAndClick(e.optionsButton);
}
await this.waitAndClick(e.logoutBtn);
}
async shareWebcam(shouldConfirmSharing = true, videoPreviewTimeout = ELEMENT_WAIT_TIME) {
const { webcamSharingEnabled } = getSettings();
const { webcamSharingEnabled } = this.settings;
test.fail(!webcamSharingEnabled, 'Webcam sharing is disabled');
if(!webcamSharingEnabled) {

View File

@ -11,6 +11,7 @@ async function generateSettingsData(page) {
settings = {
reactionsButton: settingsData.app.reactionsButton.enabled,
sharedNotesEnabled: settingsData.notes.enabled,
directLeaveButton: settingsData.app.defaultSettings.application.directLeaveButton,
// Audio
autoJoinAudioModal: settingsData.app.autoJoin,
listenOnlyMode: settingsData.app.listenOnlyMode,

View File

@ -4,7 +4,7 @@ const { openPublicChat } = require('../chat/util');
const { expect } = require("@playwright/test");
const Page = require("../core/page");
const { sleep } = require("../core/helpers");
const { ELEMENT_WAIT_EXTRA_LONG_TIME } = require("../core/constants");
const { ELEMENT_WAIT_EXTRA_LONG_TIME, ELEMENT_WAIT_LONGER_TIME } = require("../core/constants");
const { openPoll, timeInSeconds, rowFilter } = require("./util");
const { checkTextContent } = require('../core/util');
@ -118,7 +118,7 @@ class LearningDashboard extends MultiUsers {
async basicInfos() {
// Meeting Status check
await this.dashboardPage.hasText(e.meetingStatusActiveDashboard, 'Active');
await this.dashboardPage.hasText(e.meetingStatusActiveDashboard, 'Active', ELEMENT_WAIT_LONGER_TIME);
await this.dashboardPage.reloadPage();
// Meeting Time Duration check
@ -131,7 +131,7 @@ class LearningDashboard extends MultiUsers {
const timeContentGreater = await (timeLocator).textContent();
const arrayGreater = timeContentGreater.split(':').map(Number);
const secondTime = arrayGreater[1] * 3600 + arrayGreater[2] * 60 + arrayGreater[3];
await expect(secondTime).toBeGreaterThan(firstTime);
}
@ -157,8 +157,7 @@ class LearningDashboard extends MultiUsers {
}
async downloadSessionLearningDashboard(testInfo) {
await this.modPage.waitAndClick(e.optionsButton);
await this.modPage.waitAndClick(e.logout);
await this.modPage.logoutFromMeeting();
await this.modPage.waitAndClick('button');
const downloadSessionLocator = this.dashboardPage.getLocator(e.downloadSessionLearningDashboard);

View File

@ -1441,8 +1441,9 @@ Useful tools for development:
| `userdata-bbb_skip_check_audio=` | If set to `true`, the user will not see the "echo test" prompt when sharing audio | `false` |
| `userdata-bbb_skip_check_audio_on_first_join=` | (Introduced in BigBlueButton 2.3) If set to `true`, the user will not see the "echo test" when sharing audio for the first time in the session. If the user stops sharing, next time they try to share audio the echo test window will be displayed, allowing for configuration changes to be made prior to sharing audio again | `false` |
| `userdata-bbb_override_default_locale=` | (Introduced in BigBlueButton 2.3) If set to `de`, the user's browser preference will be ignored - the client will be shown in 'de' (i.e. German) regardless of the otherwise preferred locale 'en' (or other) | `null` |
| `userdata-bbb_hide_presentation_on_join` | (Introduced in BigBlueButton 2.6) If set to `true` it will make the user enter the meeting with presentation minimized, not permanent. | `false` |
| `userdata-bbb_show_animations_default` | (Introduced in BigBlueButton 2.7.4) If set to `false` the default value for the Animations toggle in Settings will be 'off' | `true` |
| `userdata-bbb_hide_presentation_on_join` | (Introduced in BigBlueButton 2.6) If set to `true` it will make the user enter the meeting with presentation minimized (Only for non-presenters), not peremanent.
| `userdata-bbb_direct_leave_button` | (Introduced in BigBlueButton 2.7) If set to `true` it will make a button to leave the meeting appear to the left of the Options menu. | `false` | | `false` |
#### Branding parameters