feat(conference) add end conference

Add the ability (for moderators) to end the meeting for everyone.
This commit is contained in:
wfleischer 2022-08-26 20:25:04 +02:00 committed by GitHub
parent 3bb581c8d9
commit 09efaecc41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 723 additions and 42 deletions

View File

@ -134,6 +134,20 @@
}
}
.hangup-menu-button {
background-color: $hangupMenuButtonColor;
@media (hover: hover) and (pointer: fine) {
&:hover {
background-color: $hangupMenuButtonHoverColor;
}
}
svg {
fill: #fff;
}
}
.profile-button-avatar {
align-items: center;
}

View File

@ -6,6 +6,8 @@
$baseFontFamily: -apple-system, BlinkMacSystemFont, 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$hangupColor:#DD3849;
$hangupHoverColor: #F25363;
$hangupMenuButtonColor:#0056E0;;
$hangupMenuButtonHoverColor: #246FE5;
$hangupFontSize: 2em;
/**

View File

@ -52,6 +52,7 @@ VirtualHost "jitmeet.example.com"
av_moderation_component = "avmoderation.jitmeet.example.com"
speakerstats_component = "speakerstats.jitmeet.example.com"
conference_duration_component = "conferenceduration.jitmeet.example.com"
end_conference_component = "endconference.jitmeet.example.com"
-- we need bosh
modules_enabled = {
"bosh";
@ -60,6 +61,7 @@ VirtualHost "jitmeet.example.com"
"speakerstats";
"external_services";
"conference_duration";
"end_conference";
"muc_lobby_rooms";
"muc_breakout_rooms";
"av_moderation";
@ -123,6 +125,9 @@ Component "speakerstats.jitmeet.example.com" "speakerstats_component"
Component "conferenceduration.jitmeet.example.com" "conference_duration_component"
muc_component = "conference.jitmeet.example.com"
Component "endconference.jitmeet.example.com" "end_conference"
muc_component = "conference.jitmeet.example.com"
Component "avmoderation.jitmeet.example.com" "av_moderation_component"
muc_component = "conference.jitmeet.example.com"

View File

@ -79,7 +79,6 @@
},
"carmode": {
"actions": {
"leaveMeeting": "اترك الاجتماع",
"selectSoundDevice": "حدد جهاز الصوت"
},
"labels": {
@ -1072,6 +1071,7 @@
"invite": "ادعُ آخرين",
"kick": "اطرد مشاركًا",
"laugh": "يضحك",
"leaveConference": "اترك الاجتماع",
"like": "رفع الإبهام متمنيا النجاح",
"linkToSalesforce": "ارتباط إلى Salesforce",
"lobbyButton": "فعِّل/عطِّل وضع غرفة الانتظار",
@ -1146,6 +1146,7 @@
"joinBreakoutRoom": "انضم إلى غرفة الجانبية",
"laugh": "يضحك",
"leaveBreakoutRoom": "اترك إلى غرفة الجانبية",
"leaveConference": "اترك الاجتماع",
"like": "رفع الإبهام متمنيا النجاح",
"linkToSalesforce": "ارتباط إلى Salesforce",
"lobbyButtonDisable": "عطِّل وضع غرفة الانتظار",

View File

@ -79,7 +79,6 @@
},
"carmode": {
"actions": {
"leaveMeeting": "Abandona la reunió",
"selectSoundDevice": "Seleccioneu l'aparell d'àudio"
},
"labels": {
@ -1039,6 +1038,7 @@
"invite": "Convida-hi persones",
"kick": "Expulsa el participant",
"laugh": "Riure",
"leaveConference": "Abandona la reunió",
"like": "Polzes amunt",
"linkToSalesforce": "Enllaç a Salesforce",
"lobbyButton": "Activa o desactiva la sala d'espera",
@ -1111,6 +1111,7 @@
"joinBreakoutRoom": "Entra a la sala de descans",
"laugh": "Riure",
"leaveBreakoutRoom": "Surt de la sala de descans",
"leaveConference": "Abandona la reunió",
"like": "Polzes amunt",
"linkToSalesforce": "Enllaç a Salesforce",
"lobbyButtonDisable": "Desactiva el mode de sala d'espera",

View File

@ -79,7 +79,6 @@
},
"carmode": {
"actions": {
"leaveMeeting": "Konferenz verlassen",
"selectSoundDevice": "Audiogerät auswählen"
},
"labels": {
@ -1066,6 +1065,7 @@
"invite": "Person einladen",
"kick": "Person entfernen",
"laugh": "Lachen",
"leaveConference": "Konferenz verlassen",
"like": "Daumen nach oben",
"linkToSalesforce": "Mit Salesforce verlinken",
"lobbyButton": "Lobbymodus ein-/ausschalten",
@ -1140,6 +1140,7 @@
"joinBreakoutRoom": "In Breakout-Raum wechseln",
"laugh": "Lachen",
"leaveBreakoutRoom": "Breakout-Raum verlassen",
"leaveConference": "Konferenz verlassen",
"like": "Daumen hoch",
"linkToSalesforce": "Mit Salesforce verknüpfen",
"lobbyButtonDisable": "Lobbymodus deaktivieren",

View File

@ -79,7 +79,6 @@
},
"carmode": {
"actions": {
"leaveMeeting": "konferencu wopušćić",
"selectSoundDevice": "nastroj za zwuk wuzwolić"
},
"labels": {
@ -1045,6 +1044,7 @@
"invite": "wobdźělnika přeprosyć",
"kick": " wobdźělnika wuzamknyć",
"laugh": "so smjeć",
"leaveConference": "konferencu wopušćić",
"like": "palc horje",
"linkToSalesforce": "ze Salesforce zwjazać",
"lobbyButton": "lobby-modus zapnyć/hasnyć",
@ -1117,6 +1117,7 @@
"joinBreakoutRoom": "do breakout rumnosće měnić",
"laugh": "so smjeć",
"leaveBreakoutRoom": "breakout rumnosć wopusćić",
"leaveConference": "konferencu wopušćić",
"like": "palc horje",
"linkToSalesforce": "ze Salesforce zwjazać",
"lobbyButtonDisable": "lobby-modus deaktiwěrować",

View File

@ -79,7 +79,6 @@
},
"carmode": {
"actions": {
"leaveMeeting": " Lascia riunione",
"selectSoundDevice": "Scegli audio"
},
"labels": {
@ -1045,6 +1044,7 @@
"invite": "Invita partecipanti",
"kick": "Espelli partecipante",
"laugh": "Ridi",
"leaveConference": " Lascia riunione",
"like": "Mi piace",
"linkToSalesforce": "Collega a Salesforce",
"lobbyButton": "Attiva/Disattiva sala d'attesa",
@ -1117,6 +1117,7 @@
"joinBreakoutRoom": "Entra in sottogruppo",
"laugh": "Ridi",
"leaveBreakoutRoom": "Lascia breakout room",
"leaveConference": " Lascia riunione",
"like": "Mi piace",
"linkToSalesforce": "Collega a Salesforce",
"lobbyButtonDisable": "Disabilita sala d'attesa",

View File

@ -79,7 +79,6 @@
},
"carmode": {
"actions": {
"leaveMeeting": " Deixar a reunião",
"selectSoundDevice": "Seleccionar dispositivo de som"
},
"labels": {
@ -1076,6 +1075,7 @@
"invite": "Convidar pessoas",
"kick": "Remover participante",
"laugh": "Risos",
"leaveConference": "Deixar a reunião",
"like": "Aprovado",
"linkToSalesforce": "Link para a Salesforce",
"lobbyButton": "Ativar/desativar sala de espera",
@ -1150,6 +1150,7 @@
"joinBreakoutRoom": "Entrar na sala",
"laugh": "Risos",
"leaveBreakoutRoom": "Sair da sala",
"leaveConference": "Deixar a reunião",
"like": "Aprovado",
"linkToSalesforce": "Link para a Salesforce",
"lobbyButtonDisable": "Desativar sala de espera",

View File

@ -78,7 +78,6 @@
},
"carmode": {
"actions": {
"leaveMeeting": " Leave meeting",
"selectSoundDevice": "Select sound device"
},
"labels": {
@ -1064,6 +1063,7 @@
"document": "Toggle shared document",
"download": "Download our apps",
"embedMeeting": "Embed meeting",
"endConference": "End meeting for all",
"expand": "Expand",
"feedback": "Leave feedback",
"fullScreen": "Toggle full screen",
@ -1074,6 +1074,7 @@
"invite": "Invite people",
"kick": "Kick participant",
"laugh": "Laugh",
"leaveConference": "Leave meeting",
"like": "Thumbs Up",
"linkToSalesforce": "Link to Salesforce",
"lobbyButton": "Enable/disable lobby mode",
@ -1136,6 +1137,7 @@
"download": "Download our apps",
"e2ee": "End-to-End Encryption",
"embedMeeting": "Embed meeting",
"endConference": "End meeting for all",
"enterFullScreen": "View full screen",
"enterTileView": "Enter tile view",
"exitFullScreen": "Exit full screen",
@ -1148,6 +1150,7 @@
"joinBreakoutRoom": "Join breakout room",
"laugh": "Laugh",
"leaveBreakoutRoom": "Leave breakout room",
"leaveConference": "Leave meeting",
"like": "Thumbs Up",
"linkToSalesforce": "Link to Salesforce",
"lobbyButtonDisable": "Disable lobby mode",

View File

@ -6,9 +6,10 @@ import {
createStartMutedConfigurationEvent,
sendAnalytics
} from '../../analytics';
import { appNavigate } from '../../app/actions';
import { endpointMessageReceived } from '../../subtitles';
import { getReplaceParticipant } from '../config/functions';
import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection';
import { JITSI_CONNECTION_CONFERENCE_KEY, disconnect } from '../connection';
import { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
import {
MEDIA_TYPE,
@ -27,6 +28,7 @@ import {
participantRoleChanged,
participantUpdated
} from '../participants';
import { toState } from '../redux';
import {
destroyLocalTracks,
getLocalTracks,
@ -75,6 +77,7 @@ import {
commonUserLeftHandling,
getConferenceOptions,
getCurrentConference,
getConferenceState,
sendLocalParticipant
} from './functions';
import logger from './logger';
@ -584,6 +587,19 @@ export function dataChannelOpened() {
};
}
/**
* Action to end a conference for all participants.
*
* @returns {Function}
*/
export function endConference() {
return async (dispatch: Dispatch<any>, getState: Function) => {
const { conference } = getConferenceState(toState(getState));
conference?.end();
};
}
/**
* Signals that we've been kicked out of the conference.
*
@ -605,6 +621,25 @@ export function kickedOut(conference: Object, participant: Object) {
};
}
/**
* Action to leave a conference.
*
* @returns {Function}
*/
export function leaveConference() {
return async (dispatch: Dispatch<any>) => {
// FIXME: these should be unified.
if (navigator.product === 'ReactNative') {
dispatch(appNavigate(undefined));
} else {
dispatch(disconnect(true));
}
};
}
/**
* Signals that the lock state of a specific JitsiConference changed.
*

View File

@ -11,10 +11,7 @@ import {
import { reloadNow } from '../../app/actions';
import { removeLobbyChatParticipant } from '../../chat/actions.any';
import { openDisplayNamePrompt } from '../../display-name';
import {
NOTIFICATION_TIMEOUT_TYPE,
showErrorNotification
} from '../../notifications';
import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification, showWarningNotification } from '../../notifications';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, connectionDisconnected } from '../connection';
import { validateJwt } from '../jwt';
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
@ -132,7 +129,7 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
const [ reason ] = error.params;
dispatch(showErrorNotification({
dispatch(showWarningNotification({
description: reason,
titleKey: 'dialog.sessTerminated'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));

View File

@ -29,9 +29,9 @@ const EndMeetingButton = () : JSX.Element => {
return (
<Button
accessibilityLabel = 'carmode.actions.leaveMeeting'
accessibilityLabel = 'toolbar.accessibilityLabel.leaveConference'
icon = { EndMeetingIcon }
labelKey = 'carmode.actions.leaveMeeting'
labelKey = 'toolbar.leaveConference'
onClick = { onSelect }
style = { styles.endMeetingButton }
type = { BUTTON_TYPES.DESTRUCTIVE } />

View File

@ -29,6 +29,16 @@ export const FULL_SCREEN_CHANGED = 'FULL_SCREEN_CHANGED';
*/
export const SET_FULL_SCREEN = 'SET_FULL_SCREEN';
/**
* The type of the (redux) action which shows/hides the hangup menu.
*
* {
* type: SET_HANGUP_MENU_VISIBLE,
* visible: boolean
* }
*/
export const SET_HANGUP_MENU_VISIBLE = 'SET_HANGUP_MENU_VISIBLE';
/**
* The type of the redux action that toggles whether the overflow menu(s) should be shown as drawers.
*/

View File

@ -9,6 +9,7 @@ import {
CLEAR_TOOLBOX_TIMEOUT,
FULL_SCREEN_CHANGED,
SET_FULL_SCREEN,
SET_HANGUP_MENU_VISIBLE,
SET_OVERFLOW_DRAWER,
SET_OVERFLOW_MENU_VISIBLE,
SET_TOOLBAR_HOVERED,
@ -188,6 +189,22 @@ export function clearToolboxTimeout(): Object {
};
}
/**
* Shows/hides the hangup menu.
*
* @param {boolean} visible - True to show it or false to hide it.
* @returns {{
* type: SET_HANGUP_MENU_VISIBLE,
* visible: boolean
* }}
*/
export function setHangupMenuVisible(visible: boolean): Object {
return {
type: SET_HANGUP_MENU_VISIBLE,
visible
};
}
/**
* Shows/hides the overflow menu.
*

View File

@ -3,8 +3,7 @@
import _ from 'lodash';
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { appNavigate } from '../../app/actions';
import { disconnect } from '../../base/connection';
import { leaveConference } from '../../base/conference/actions';
import { translate } from '../../base/i18n';
import { connect } from '../../base/redux';
import { AbstractHangupButton } from '../../base/toolbox/components';
@ -44,13 +43,7 @@ class HangupButton extends AbstractHangupButton<Props, *> {
this._hangup = _.once(() => {
sendAnalytics(createToolbarEvent('hangup'));
// FIXME: these should be unified.
if (navigator.product === 'ReactNative') {
this.props.dispatch(appNavigate(undefined));
} else {
this.props.dispatch(disconnect(true));
}
this.props.dispatch(leaveConference());
});
}

View File

@ -0,0 +1,83 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
// @ts-ignore
import { createBreakoutRoomsEvent, createToolbarEvent, sendAnalytics } from '../../../analytics';
// @ts-ignore
import { appNavigate } from '../../../app/actions';
// @ts-ignore
import { ColorSchemeRegistry } from '../../../base/color-scheme';
// @ts-ignore
import { endConference } from '../../../base/conference';
// @ts-ignore
import { hideSheet } from '../../../base/dialog';
// @ts-ignore
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
// @ts-ignore
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants';
// @ts-ignore
import { moveToRoom } from '../../../breakout-rooms/actions';
// @ts-ignore
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
/**
* Menu presenting options to leave a room or meeting and to end meeting.
*
* @returns {JSX.Element} - The hangup menu.
*/
function HangupMenu() {
const dispatch = useDispatch();
const _styles = useSelector(state => ColorSchemeRegistry.get(state, 'Toolbox'));
const inBreakoutRoom = useSelector(isInBreakoutRoom);
const isModerator = useSelector(state => getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR);
const { DESTRUCTIVE, SECONDARY } = BUTTON_TYPES;
const handleEndConference = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createToolbarEvent('endmeeting'));
dispatch(endConference());
}, [ hideSheet ]);
const handleLeaveConference = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createToolbarEvent('hangup'));
dispatch(appNavigate(undefined));
}, [ hideSheet ]);
const handleLeaveBreakoutRoom = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createBreakoutRoomsEvent('leave'));
dispatch(moveToRoom());
}, [ hideSheet ]);
return (
<BottomSheet>
<View style = { _styles.hangupMenuContainer }>
{ isModerator && <Button
accessibilityLabel = 'toolbar.endConference'
labelKey = 'toolbar.endConference'
onClick = { handleEndConference }
style = { _styles.hangupButton }
type = { DESTRUCTIVE } /> }
<Button
accessibilityLabel = 'toolbar.leaveConference'
labelKey = 'toolbar.leaveConference'
onClick = { handleLeaveConference }
style = { _styles.hangupButton }
type = { SECONDARY } />
{ inBreakoutRoom && <Button
accessibilityLabel = 'breakoutRooms.actions.leaveBreakoutRoom'
labelKey = 'breakoutRooms.actions.leaveBreakoutRoom'
onClick = { handleLeaveBreakoutRoom }
style = { _styles.hangupButton }
type = { SECONDARY } /> }
</View>
</BottomSheet>
);
}
export default HangupMenu;

View File

@ -0,0 +1,36 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
// @ts-ignore
import { openSheet } from '../../../base/dialog';
// @ts-ignore
import { IconHangup } from '../../../base/icons';
import IconButton from '../../../base/react/components/native/IconButton';
import { BUTTON_TYPES } from '../../../base/ui/constants';
import HangupMenu from './HangupMenu';
/**
* Button for showing the hangup menu.
*
* @returns {JSX.Element} - The hangup menu button.
*/
const HangupMenuButton = () : JSX.Element => {
const dispatch = useDispatch();
const onSelect = useCallback(() => {
dispatch(openSheet(HangupMenu));
}, [ dispatch ]);
return (
<IconButton
accessibilityLabel = 'toolbar.accessibilityLabel.hangup'
onPress = { onSelect }
src = { IconHangup }
type = { BUTTON_TYPES.PRIMARY } />
);
};
export default HangupMenuButton;

View File

@ -18,6 +18,7 @@ import AudioMuteButton from '../AudioMuteButton';
import HangupButton from '../HangupButton';
import VideoMuteButton from '../VideoMuteButton';
import HangupMenuButton from './HangupMenuButton';
import OverflowMenuButton from './OverflowMenuButton';
import RaiseHandButton from './RaiseHandButton';
import styles from './styles';
@ -27,6 +28,11 @@ import styles from './styles';
*/
type Props = {
/**
* Whether the end conference feature is supported.
*/
_endConferenceSupported: boolean,
/**
* Whether or not the reactions feature is enabled.
*/
@ -55,14 +61,14 @@ type Props = {
* @returns {React$Element}.
*/
function Toolbox(props: Props) {
const { _reactionsEnabled, _styles, _visible, _width } = props;
const { _endConferenceSupported, _reactionsEnabled, _styles, _visible, _width } = props;
if (!_visible) {
return null;
}
const bottomEdge = Platform.OS === 'ios' && _visible;
const { buttonStylesBorderless, hangupButtonStyles, toggledButtonStyles } = _styles;
const { buttonStylesBorderless, hangupButtonStyles, hangupMenuButtonStyles, toggledButtonStyles } = _styles;
const additionalButtons = getMovableButtons(_width);
const backgroundToggledStyle = {
...toggledButtonStyles,
@ -110,8 +116,13 @@ function Toolbox(props: Props) {
<OverflowMenuButton
styles = { buttonStylesBorderless }
toggledStyles = { toggledButtonStyles } />
<HangupButton
styles = { hangupButtonStyles } />
{ _endConferenceSupported
? <HangupMenuButton
styles = { hangupMenuButtonStyles }
toggledStyles = { toggledButtonStyles } />
: <HangupButton
styles = { hangupButtonStyles } />
}
</SafeAreaView>
</View>
);
@ -127,7 +138,11 @@ function Toolbox(props: Props) {
* @returns {Props}
*/
function _mapStateToProps(state: Object): Object {
const { conference } = state['features/base/conference'];
const endConferenceSupported = conference?.isEndConferenceSupported();
return {
_endConferenceSupported: Boolean(endConferenceSupported),
_styles: ColorSchemeRegistry.get(state, 'Toolbox'),
_visible: isToolboxVisible(state),
_width: state['features/base/responsive-ui'].clientWidth,

View File

@ -133,6 +133,17 @@ ColorSchemeRegistry.register('Toolbox', {
backgroundColor: BaseTheme.palette.ui13
},
hangupMenuContainer: {
marginHorizontal: BaseTheme.spacing[2],
marginVertical: BaseTheme.spacing[2]
},
hangupButton: {
flex: 1,
marginHorizontal: BaseTheme.spacing[2],
marginVertical: BaseTheme.spacing[2]
},
hangupButtonStyles: {
iconStyle: whiteToolbarButtonIcon,
style: {

View File

@ -0,0 +1,38 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
// @ts-ignore
import { endConference } from '../../../base/conference';
// @ts-ignore
import { isLocalParticipantModerator } from '../../../base/participants';
import Button from '../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants';
// @ts-ignore
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
/**
* Button to end the conference for all participants.
*
* @returns {JSX.Element} - The end conference button.
*/
export const EndConferenceButton = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const _isLocalParticipantModerator = useSelector(isLocalParticipantModerator);
const _isInBreakoutRoom = useSelector(isInBreakoutRoom);
const onEndConference = useCallback(() => {
dispatch(endConference());
}, [ dispatch ]);
return (<>
{ !_isInBreakoutRoom && _isLocalParticipantModerator && <Button
accessibilityLabel = { t('toolbar.accessibilityLabel.endConference') }
fullWidth = { true }
label = { t('toolbar.endConference') }
onClick = { onEndConference }
type = { BUTTON_TYPES.DESTRUCTIVE } /> }
</>);
};

View File

@ -0,0 +1,130 @@
/* eslint-disable lines-around-comment */
import InlineDialog from '@atlaskit/inline-dialog';
import React, { Component } from 'react';
// @ts-ignore
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
// @ts-ignore
import { translate } from '../../../base/i18n';
import HangupToggleButton from './HangupToggleButton';
/**
* The type of the React {@code Component} props of {@link HangupMenuButton}.
*/
type Props = {
/**
* ID of the menu that is controlled by this button.
*/
ariaControls: String,
/**
* A child React Element to display within {@code InlineDialog}.
*/
children: React.ReactNode,
/**
* Whether or not the HangupMenu popover should display.
*/
isOpen: boolean,
/**
* Callback to change the visibility of the hangup menu.
*/
onVisibilityChange: Function,
/**
* Invoked to obtain translated strings.
*/
t: Function,
};
/**
* A React {@code Component} for opening or closing the {@code HangupMenu}.
*
* @augments Component
*/
class HangupMenuButton extends Component<Props> {
/**
* Initializes a new {@code HangupMenuButton} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onCloseDialog = this._onCloseDialog.bind(this);
this._toggleDialogVisibility
= this._toggleDialogVisibility.bind(this);
this._onEscClick = this._onEscClick.bind(this);
}
/**
* Click handler for the more actions entries.
*
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onEscClick(event: KeyboardEvent) {
if (event.key === 'Escape' && this.props.isOpen) {
event.preventDefault();
event.stopPropagation();
this._onCloseDialog();
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { children, isOpen } = this.props;
return (
<div className = 'toolbox-button-wth-dialog context-menu'>
<InlineDialog
content = { children }
isOpen = { isOpen }
onClose = { this._onCloseDialog }
placement = 'top-end'>
<HangupToggleButton
customClass = 'hangup-menu-button'
handleClick = { this._toggleDialogVisibility }
isOpen = { isOpen }
onKeyDown = { this._onEscClick } />
</InlineDialog>
</div>
);
}
/**
* Callback invoked when {@code InlineDialog} signals that it should be
* close.
*
* @private
* @returns {void}
*/
_onCloseDialog() {
this.props.onVisibilityChange(false);
}
/**
* Callback invoked to signal that an event has occurred that should change
* the visibility of the {@code InlineDialog} component.
*
* @private
* @returns {void}
*/
_toggleDialogVisibility() {
sendAnalytics(createToolbarEvent('hangup'));
this.props.onVisibilityChange(!this.props.isOpen);
}
}
export default translate(HangupMenuButton);

View File

@ -0,0 +1,75 @@
/* eslint-disable lines-around-comment */
// @ts-ignore
import { translate } from '../../../base/i18n';
// @ts-ignore
import { IconClose, IconHangup } from '../../../base/icons';
// @ts-ignore
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
/**
* The type of the React {@code Component} props of {@link HangupToggleButton}.
*/
type Props = AbstractButtonProps & {
/**
* Whether the more options menu is open.
*/
isOpen: boolean,
/**
* External handler for key down action.
*/
onKeyDown: Function,
};
/**
* Implementation of a button for toggling the hangup menu.
*/
class HangupToggleButton extends AbstractButton<Props, any, any> {
accessibilityLabel = 'toolbar.accessibilityLabel.hangup';
icon = IconHangup;
label = 'toolbar.hangup';
toggledIcon = IconClose;
toggledLabel = 'toolbar.hangup';
props: Props;
/**
* Retrieves tooltip dynamically.
*/
get tooltip() {
return 'toolbar.hangup';
}
/**
* Required by linter due to AbstractButton overwritten prop being writable.
*
* @param {string} _value - The value.
*/
set tooltip(_value) {
// Unused.
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props.isOpen;
}
/**
* Indicates whether a key was pressed.
*
* @override
* @protected
* @returns {boolean}
*/
_onKeyDown() {
this.props.onKeyDown();
}
}
export default translate(HangupToggleButton);

View File

@ -0,0 +1,35 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
// @ts-ignore
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
// @ts-ignore
import { leaveConference } from '../../../base/conference/actions';
import Button from '../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants';
/**
* Button to leave the conference.
*
* @returns {JSX.Element} - The leave conference button.
*/
export const LeaveConferenceButton = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const onLeaveConference = useCallback(() => {
sendAnalytics(createToolbarEvent('hangup'));
dispatch(leaveConference());
}, [ dispatch ]);
return (
<Button
accessibilityLabel = { t('toolbar.accessibilityLabel.leaveConference') }
fullWidth = { true }
label = { t('toolbar.leaveConference') }
onClick = { onLeaveConference }
type = { BUTTON_TYPES.SECONDARY } />
);
};

View File

@ -108,6 +108,7 @@ import { VideoBackgroundButton, toggleBackgroundEffect } from '../../../virtual-
import { VIRTUAL_BACKGROUND_TYPE } from '../../../virtual-background/constants';
import {
setFullScreen,
setHangupMenuVisible,
setOverflowMenuVisible,
setToolbarHovered,
showToolbox
@ -127,8 +128,11 @@ import HelpButton from '../HelpButton';
import AudioSettingsButton from './AudioSettingsButton';
// @ts-ignore
import DockIframeButton from './DockIframeButton';
import { EndConferenceButton } from './EndConferenceButton';
// @ts-ignore
import FullscreenButton from './FullscreenButton';
import HangupMenuButton from './HangupMenuButton';
import { LeaveConferenceButton } from './LeaveConferenceButton';
// @ts-ignore
import LinkToSalesforceButton from './LinkToSalesforceButton';
// @ts-ignore
@ -199,6 +203,11 @@ interface Props extends WithTranslation {
*/
_disabled: boolean,
/**
* Whether the end conference feature is supported.
*/
_endConferenceSupported: boolean,
/**
* Whether or not call feedback can be sent.
*/
@ -214,12 +223,17 @@ interface Props extends WithTranslation {
*/
_gifsEnabled: boolean,
/**
* Whether the hangup menu is visible.
*/
_hangupMenuVisible: boolean,
/**
* Whether the app has Salesforce integration.
*/
_hasSalesforce: boolean,
/**
/**
* Whether or not the app is running in an ios mobile browser.
*/
_isIosMobile: boolean,
@ -335,6 +349,15 @@ const styles = () => {
right: 'auto',
maxHeight: 'inherit',
margin: 0
},
hangupMenu: {
position: 'relative',
right: 'auto',
display: 'flex',
flexDirection: 'column',
rowGap: '8px',
margin: 0,
padding: '16px'
}
};
};
@ -357,6 +380,7 @@ class Toolbox extends Component<Props> {
// Bind event handlers so they are only bound once per instance.
this._onMouseOut = this._onMouseOut.bind(this);
this._onMouseOver = this._onMouseOver.bind(this);
this._onSetHangupVisible = this._onSetHangupVisible.bind(this);
this._onSetOverflowVisible = this._onSetOverflowVisible.bind(this);
this._onTabIn = this._onTabIn.bind(this);
@ -482,7 +506,7 @@ class Toolbox extends Component<Props> {
* @inheritdoc
*/
componentDidUpdate(prevProps: Props) {
const { _dialog, dispatch } = this.props;
const { _dialog, _visible, dispatch } = this.props;
if (prevProps._overflowMenuVisible
@ -491,6 +515,12 @@ class Toolbox extends Component<Props> {
this._onSetOverflowVisible(false);
dispatch(setToolbarHovered(false));
}
if (prevProps._hangupMenuVisible
&& prevProps._visible
&& !_visible) {
this._onSetHangupVisible(false);
dispatch(setToolbarHovered(false));
}
}
/**
@ -535,7 +565,7 @@ class Toolbox extends Component<Props> {
}
/**
* Key handler for overflow menu.
* Key handler for overflow/hangup menus.
*
* @param {KeyboardEvent} e - Esc key click to close the popup.
* @returns {void}
@ -543,10 +573,23 @@ class Toolbox extends Component<Props> {
_onEscKey(e: React.KeyboardEvent) {
if (e.key === 'Escape') {
e.stopPropagation();
this._closeHangupMenuIfOpen();
this._closeOverflowMenuIfOpen();
}
}
/**
* Closes the hangup menu if opened.
*
* @private
* @returns {void}
*/
_closeHangupMenuIfOpen() {
const { dispatch, _hangupMenuVisible } = this.props;
_hangupMenuVisible && dispatch(setHangupMenuVisible(false));
}
/**
* Closes the overflow menu if opened.
*
@ -1012,6 +1055,19 @@ class Toolbox extends Component<Props> {
this.props.dispatch(setToolbarHovered(true));
}
/**
* Sets the visibility of the hangup menu.
*
* @param {boolean} visible - Whether or not the hangup menu should be
* displayed.
* @private
* @returns {void}
*/
_onSetHangupVisible(visible: boolean) {
this.props.dispatch(setHangupMenuVisible(visible));
this.props.dispatch(setToolbarHovered(visible));
}
/**
* Sets the visibility of the overflow menu.
*
@ -1307,6 +1363,8 @@ class Toolbox extends Component<Props> {
*/
_renderToolboxContent() {
const {
_endConferenceSupported,
_hangupMenuVisible,
_isMobile,
_overflowDrawer,
_overflowMenuVisible,
@ -1383,12 +1441,30 @@ class Toolbox extends Component<Props> {
</OverflowMenuButton>
)}
<HangupButton
buttonKey = 'hangup'
customClass = 'hangup-button'
key = 'hangup-button'
notifyMode = { this._getButtonNotifyMode('hangup') }
visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
{ isToolbarButtonEnabled('hangup', _toolbarButtons) && (
_endConferenceSupported
? <HangupMenuButton
ariaControls = 'hangup-menu'
isOpen = { _hangupMenuVisible }
key = 'hangup-menu'
onVisibilityChange = { this._onSetHangupVisible }>
<ContextMenu
accessibilityLabel = { t(toolbarAccLabel) }
className = { classes.hangupMenu }
hidden = { false }
inDrawer = { _overflowDrawer }
onKeyDown = { this._onEscKey }>
<EndConferenceButton />
<LeaveConferenceButton />
</ContextMenu>
</HangupMenuButton>
: <HangupButton
buttonKey = 'hangup'
customClass = 'hangup-button'
key = 'hangup-button'
notifyMode = { this._getButtonNotifyMode('hangup') }
visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
)}
</div>
</div>
</div>
@ -1407,6 +1483,7 @@ class Toolbox extends Component<Props> {
*/
function _mapStateToProps(state: any, ownProps: Partial<Props>) {
const { conference } = state['features/base/conference'];
const endConferenceSupported = conference?.isEndConferenceSupported();
const {
buttonsWithNotifyClick,
callStatsID,
@ -1416,6 +1493,7 @@ function _mapStateToProps(state: any, ownProps: Partial<Props>) {
} = state['features/base/config'];
const {
fullScreen,
hangupMenuVisible,
overflowMenuVisible,
overflowDrawer
} = state['features/toolbox'];
@ -1434,6 +1512,7 @@ function _mapStateToProps(state: any, ownProps: Partial<Props>) {
_desktopSharingButtonDisabled: isDesktopShareButtonDisabled(state),
_dialog: Boolean(state['features/base/dialog'].component),
_disabled: Boolean(iAmRecorder || iAmSipGateway),
_endConferenceSupported: Boolean(endConferenceSupported),
_feedbackConfigured: Boolean(callStatsID),
_fullScreen: fullScreen,
_gifsEnabled: isGifEnabled(state),
@ -1442,6 +1521,7 @@ function _mapStateToProps(state: any, ownProps: Partial<Props>) {
_isMobile: isMobileBrowser(),
_isVpaasMeeting: isVpaasMeeting(state),
_hasSalesforce: isSalesforceEnabled(state),
_hangupMenuVisible: hangupMenuVisible,
_localParticipantID: localParticipant?.id,
_localVideo: localVideo,
_multiStreamModeEnabled: getMultipleVideoSendingSupportFeatureFlag(state),

View File

@ -5,6 +5,7 @@ import { ReducerRegistry, set } from '../base/redux';
import {
CLEAR_TOOLBOX_TIMEOUT,
FULL_SCREEN_CHANGED,
SET_HANGUP_MENU_VISIBLE,
SET_OVERFLOW_DRAWER,
SET_OVERFLOW_MENU_VISIBLE,
SET_TOOLBAR_HOVERED,
@ -26,6 +27,13 @@ const INITIAL_STATE = {
*/
enabled: true,
/**
* The indicator which determines whether the hangup menu is visible.
*
* @type {boolean}
*/
hangupMenuVisible: false,
/**
* The indicator which determines whether a Toolbar in the Toolbox is
* hovered.
@ -81,6 +89,12 @@ ReducerRegistry.register(
fullScreen: action.fullScreen
};
case SET_HANGUP_MENU_VISIBLE:
return {
...state,
hangupMenuVisible: action.visible
};
case SET_OVERFLOW_DRAWER:
return {
...state,

View File

@ -0,0 +1,84 @@
-- This module is added under the main virtual host domain
--
-- VirtualHost "jitmeet.example.com"
-- modules_enabled = {
-- "end_conference"
-- }
-- end_conference_component = "endconference.jitmeet.example.com"
--
-- Component "endconference.jitmeet.example.com" "end_conference"
-- muc_component = muc.jitmeet.example.com
--
local get_room_by_name_and_subdomain = module:require 'util'.get_room_by_name_and_subdomain;
local END_CONFERENCE_REASON = 'The meeting has been terminated';
local end_conference_component = module:get_option_string('end_conference_component', 'endconference.'..module.host);
if end_conference_component == nil then
log('error', 'No end_conference_component specified.');
return;
end
-- Advertise end conference so client can pick up the address and use it
module:add_identity('component', 'end_conference', end_conference_component);
module:depends("jitsi_session");
local muc_component_host = module:get_option_string('muc_component');
if muc_component_host == nil then
log('error', 'No muc_component specified. No muc to operate on!');
return;
end
module:log('info', 'Starting end_conference for %s', muc_component_host);
-- receives messages from clients to the component to end a conference
function on_message(event)
local session = event.origin;
-- Check the type of the incoming stanza to avoid loops:
if event.stanza.attr.type == 'error' then
return; -- We do not want to reply to these, so leave.
end
if not session or not session.jitsi_web_query_room then
return false;
end
local moderation_command = event.stanza:get_child('end_conference');
if moderation_command then
-- get room name with tenant and find room
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
if not room then
module:log('warn', 'No room found found for %s/%s',
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
return false;
end
-- check that the participant requesting is a moderator and is an occupant in the room
local from = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(from);
if not occupant then
log('warn', 'No occupant %s found for %s', from, room.jid);
return false;
end
if occupant.role ~= 'moderator' then
log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
return false;
end
-- destroy the room
room:destroy(nil, END_CONFERENCE_REASON);
log('info', 'Room %s destroyed by occupant %s', room.jid, from);
return true;
end
-- return error
return false
end
-- we will receive messages from the clients
module:hook('message/host', on_message);

View File

@ -213,7 +213,7 @@ end
function destroy_breakout_room(room_jid, message)
local main_room, main_room_jid = get_main_room(room_jid);
if room_jid == main_room_jid or not main_room then
if room_jid == main_room_jid then
return;
end
@ -221,7 +221,7 @@ function destroy_breakout_room(room_jid, message)
if breakout_room then
message = message or 'Breakout room removed.';
breakout_room:destroy(main_room_jid, message);
breakout_room:destroy(main_room and main_room_jid or nil, message);
end
if main_room then
if main_room._data.breakout_rooms then
@ -418,10 +418,8 @@ function on_main_room_destroyed(event)
return;
end
local message = 'Conference ended.';
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
destroy_breakout_room(breakout_room_jid, message)
destroy_breakout_room(breakout_room_jid, event.reason)
end
end