feat(lobby,notifications) refactor lobby notifications

This commit is contained in:
Tudor D. Pop 2022-01-20 16:26:03 +02:00 committed by GitHub
parent a84130d67d
commit 28f5ddc81d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 143 additions and 220 deletions

View File

@ -41,64 +41,6 @@
} }
} }
#notification-participant-list {
background-color: $newToolbarBackgroundColor;
border: 1px solid rgba(255, 255, 255, .4);
border-radius: 8px;
left: 0;
margin: 20px;
max-height: 600px;
overflow: hidden;
overflow-y: auto;
position: fixed;
top: 30px;
z-index: $toolbarZ + 1;
&:empty {
border: none;
}
&.toolbox-visible {
// Same as toolbox subject position
top: 120px;
}
.title {
background-color: rgba(0, 0, 0, .2);
font-size: 1.2em;
padding: 15px
}
button {
align-self: stretch;
margin-bottom: 8px 0;
padding: 12px;
transition: .2s transform ease;
&:disabled {
opacity: .5;
}
&:hover {
transform: scale(1.05);
&:disabled {
transform: none;
}
}
&.borderLess {
background-color: transparent;
border-width: 0;
}
&.primary {
background-color: rgb(3, 118, 218);
border-width: 0;
}
}
}
.knocking-participants-container { .knocking-participants-container {
list-style-type: none; list-style-type: none;
padding: 0 15px 15px 15px; padding: 0 15px 15px 15px;

View File

@ -642,6 +642,8 @@
"oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ", "oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ",
"oldElectronClientDescription2": "latest build", "oldElectronClientDescription2": "latest build",
"oldElectronClientDescription3": " now!", "oldElectronClientDescription3": " now!",
"participantWantsToJoin": "Wants to join the meeting",
"participantsWantToJoin": "Want to join the meeting",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant", "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant", "passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
"raiseHandAction": "Raise hand", "raiseHandAction": "Raise hand",
@ -661,7 +663,9 @@
"videoMutedRemotelyDescription": "You can always turn it on again.", "videoMutedRemotelyDescription": "You can always turn it on again.",
"videoMutedRemotelyTitle": "Your video has been turned off by {{participantDisplayName}}", "videoMutedRemotelyTitle": "Your video has been turned off by {{participantDisplayName}}",
"videoUnmuteBlockedDescription": "Camera unmute and desktop sharing operation have been temporarily blocked because of system limits.", "videoUnmuteBlockedDescription": "Camera unmute and desktop sharing operation have been temporarily blocked because of system limits.",
"videoUnmuteBlockedTitle": "Camera unmute and desktop sharing blocked!" "videoUnmuteBlockedTitle": "Camera unmute and desktop sharing blocked!",
"viewLobby": "View lobby",
"waitingParticipants": "{{waitingParticipants}} people"
}, },
"participantsPane": { "participantsPane": {
"actions": { "actions": {

View File

@ -20,7 +20,7 @@ import {
} from '../../../filmstrip'; } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite'; import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video'; import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList } from '../../../lobby'; import { KnockingParticipantList } from '../../../lobby/components/native';
import { getIsLobbyVisible } from '../../../lobby/functions'; import { getIsLobbyVisible } from '../../../lobby/functions';
import { BackButtonRegistry } from '../../../mobile/back-button'; import { BackButtonRegistry } from '../../../mobile/back-button';
import { Captions } from '../../../subtitles'; import { Captions } from '../../../subtitles';

View File

@ -14,10 +14,9 @@ import { Chat } from '../../../chat';
import { Filmstrip } from '../../../filmstrip'; import { Filmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite'; import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video'; import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList, LobbyScreen } from '../../../lobby'; import { LobbyScreen } from '../../../lobby';
import { getIsLobbyVisible } from '../../../lobby/functions'; import { getIsLobbyVisible } from '../../../lobby/functions';
import { ParticipantsPane } from '../../../participants-pane/components/web'; import { ParticipantsPane } from '../../../participants-pane/components/web';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin'; import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import { toggleToolboxVisible } from '../../../toolbox/actions.any'; import { toggleToolboxVisible } from '../../../toolbox/actions.any';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web'; import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
@ -72,11 +71,6 @@ type Props = AbstractProps & {
*/ */
_backgroundAlpha: number, _backgroundAlpha: number,
/**
* If participants pane is visible or not.
*/
_isParticipantsPaneVisible: boolean,
/** /**
* The CSS class to apply to the root of {@link Conference} to modify the * The CSS class to apply to the root of {@link Conference} to modify the
* application layout. * application layout.
@ -216,7 +210,6 @@ class Conference extends AbstractConference<Props, *> {
*/ */
render() { render() {
const { const {
_isParticipantsPaneVisible,
_layoutClassName, _layoutClassName,
_notificationsVisible, _notificationsVisible,
_overflowDrawer, _overflowDrawer,
@ -242,10 +235,6 @@ class Conference extends AbstractConference<Props, *> {
id = 'videospace' id = 'videospace'
onTouchStart = { this._onVidespaceTouchStart }> onTouchStart = { this._onVidespaceTouchStart }>
<LargeVideo /> <LargeVideo />
{!_isParticipantsPaneVisible
&& <div id = 'notification-participant-list'>
<KnockingParticipantList />
</div>}
<Filmstrip /> <Filmstrip />
</div> </div>
@ -401,7 +390,6 @@ function _mapStateToProps(state) {
return { return {
...abstractMapStateToProps(state), ...abstractMapStateToProps(state),
_backgroundAlpha: backgroundAlpha, _backgroundAlpha: backgroundAlpha,
_isParticipantsPaneVisible: getParticipantsPaneOpen(state),
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)], _layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
_mouseMoveCallbackInterval: mouseMoveCallbackInterval, _mouseMoveCallbackInterval: mouseMoveCallbackInterval,
_overflowDrawer: overflowDrawer, _overflowDrawer: overflowDrawer,

View File

@ -1,78 +0,0 @@
// @flow
import { PureComponent } from 'react';
import { isLocalParticipantModerator } from '../../base/participants';
import { setKnockingParticipantApproval } from '../actions';
import { getKnockingParticipants, getLobbyEnabled } from '../functions';
export type Props = {
/**
* The list of participants.
*/
_participants: Array<Object>,
/**
* True if the list should be rendered.
*/
_visible: boolean,
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
/**
* Abstract class to encapsulate the platform common code of the {@code KnockingParticipantList}.
*/
export default class AbstractKnockingParticipantList<P: Props = Props> extends PureComponent<P> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
this._onRespondToParticipant = this._onRespondToParticipant.bind(this);
}
_onRespondToParticipant: (string, boolean) => Function;
/**
* Function that constructs a callback for the response handler button.
*
* @param {string} id - The id of the knocking participant.
* @param {boolean} approve - The response for the knocking.
* @returns {Function}
*/
_onRespondToParticipant(id, approve) {
return () => {
this.props.dispatch(setKnockingParticipantApproval(id, approve));
};
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
export function mapStateToProps(state: Object): $Shape<Props> {
const lobbyEnabled = getLobbyEnabled(state);
const knockingParticipants = getKnockingParticipants(state);
return {
_participants: knockingParticipants,
_visible: lobbyEnabled && isLocalParticipantModerator(state)
&& Boolean(knockingParticipants && knockingParticipants.length)
};
}

View File

@ -1,23 +1,60 @@
// @flow // @flow
import React from 'react'; import React, { PureComponent } from 'react';
import { ScrollView, Text, View, TouchableOpacity } from 'react-native'; import { ScrollView, Text, View, TouchableOpacity } from 'react-native';
import { Avatar } from '../../../base/avatar'; import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { setKnockingParticipantApproval } from '../../actions';
import { HIDDEN_EMAILS } from '../../constants'; import { HIDDEN_EMAILS } from '../../constants';
import AbstractKnockingParticipantList, { import { getKnockingParticipants, getLobbyEnabled } from '../../functions';
mapStateToProps as abstractMapStateToProps,
type Props
} from '../AbstractKnockingParticipantList';
import styles from './styles'; import styles from './styles';
/**
* Props type of the component.
*/
export type Props = {
/**
* The list of participants.
*/
_participants: Array<Object>,
/**
* True if the list should be rendered.
*/
_visible: boolean,
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
/** /**
* Component to render a list for the actively knocking participants. * Component to render a list for the actively knocking participants.
*/ */
class KnockingParticipantList extends AbstractKnockingParticipantList { class KnockingParticipantList extends PureComponent<Props> {
/**
* Instantiates a new component.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this._onRespondToParticipant = this._onRespondToParticipant.bind(this);
}
/** /**
* Implements {@code PureComponent#render}. * Implements {@code PureComponent#render}.
* *
@ -78,6 +115,19 @@ class KnockingParticipantList extends AbstractKnockingParticipantList {
} }
_onRespondToParticipant: (string, boolean) => Function; _onRespondToParticipant: (string, boolean) => Function;
/**
* Function that constructs a callback for the response handler button.
*
* @param {string} id - The id of the knocking participant.
* @param {boolean} approve - The response for the knocking.
* @returns {Function}
*/
_onRespondToParticipant(id, approve) {
return () => {
this.props.dispatch(setKnockingParticipantApproval(id, approve));
};
}
} }
/** /**
@ -86,14 +136,15 @@ class KnockingParticipantList extends AbstractKnockingParticipantList {
* @param {Object} state - The Redux state. * @param {Object} state - The Redux state.
* @returns {Props} * @returns {Props}
*/ */
function _mapStateToProps(state: Object): $Shape<Props> { function _mapStateToProps(state): Object {
const abstractProps = abstractMapStateToProps(state); const lobbyEnabled = getLobbyEnabled(state);
const knockingParticipants = getKnockingParticipants(state);
return { return {
...abstractProps, _visible: lobbyEnabled && isLocalParticipantModerator(state),
// On mobile we only show a portion of the list for screen real estate reasons // On mobile we only show a portion of the list for screen real estate reasons
_participants: abstractProps._participants.slice(0, 2) _participants: knockingParticipants.slice(0, 2)
}; };
} }

View File

@ -1,57 +0,0 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import NotificationWithParticipants from '../../../notifications/components/web/NotificationWithParticipants';
import { approveKnockingParticipant, rejectKnockingParticipant } from '../../actions';
import AbstractKnockingParticipantList, {
mapStateToProps as abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractKnockingParticipantList';
type Props = AbstractProps & {
/**
* True if the toolbox is visible, so we need to adjust the position.
*/
_toolboxVisible: boolean
};
/**
* Component to render a list for the actively knocking participants.
*/
class KnockingParticipantList extends AbstractKnockingParticipantList<Props> {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _participants, _visible, t } = this.props;
if (!_visible) {
return null;
}
return (
<div id = 'knocking-participant-list'>
<div className = 'title'>
{ t('lobby.knockingParticipantList') }
</div>
<NotificationWithParticipants
approveButtonText = { t('lobby.allow') }
onApprove = { approveKnockingParticipant }
onReject = { rejectKnockingParticipant }
participants = { _participants }
rejectButtonText = { t('lobby.reject') }
testIdPrefix = 'lobby' />
</div>
);
}
_onRespondToParticipant: (string, boolean) => Function;
}
export default translate(connect(abstractMapStateToProps)(KnockingParticipantList));

View File

@ -1,5 +1,4 @@
// @flow // @flow
export { default as KnockingParticipantList } from './KnockingParticipantList';
export { default as LobbySection } from './LobbySection'; export { default as LobbySection } from './LobbySection';
export { default as LobbyScreen } from './LobbyScreen'; export { default as LobbyScreen } from './LobbyScreen';

View File

@ -1,5 +1,6 @@
// @flow // @flow
import i18n from 'i18next';
import { batch } from 'react-redux'; import { batch } from 'react-redux';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
@ -13,11 +14,17 @@ import { getFirstLoadableAvatarUrl, getParticipantDisplayName } from '../base/pa
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds'; import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { isTestModeEnabled } from '../base/testing'; import { isTestModeEnabled } from '../base/testing';
import { approveKnockingParticipant, rejectKnockingParticipant } from '../lobby/actions';
import { import {
LOBBY_NOTIFICATION_ID,
NOTIFICATION_ICON,
NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TIMEOUT_TYPE,
NOTIFICATION_TYPE, NOTIFICATION_TYPE,
hideNotification,
showNotification showNotification
} from '../notifications'; } from '../notifications';
import { open as openParticipantsPane } from '../participants-pane/actions';
import { getParticipantsPaneOpen } from '../participants-pane/functions';
import { shouldAutoKnock } from '../prejoin/functions'; import { shouldAutoKnock } from '../prejoin/functions';
import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED } from './actionTypes'; import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED } from './actionTypes';
@ -31,6 +38,7 @@ import {
setPasswordJoinFailed setPasswordJoinFailed
} from './actions'; } from './actions';
import { KNOCKING_PARTICIPANT_SOUND_ID } from './constants'; import { KNOCKING_PARTICIPANT_SOUND_ID } from './constants';
import { getKnockingParticipants } from './functions';
import { KNOCKING_PARTICIPANT_FILE } from './sounds'; import { KNOCKING_PARTICIPANT_FILE } from './sounds';
declare var APP: Object; declare var APP: Object;
@ -81,6 +89,55 @@ StateListenerRegistry.register(
}) })
); );
dispatch(playSound(KNOCKING_PARTICIPANT_SOUND_ID)); dispatch(playSound(KNOCKING_PARTICIPANT_SOUND_ID));
const isParticipantsPaneVisible = getParticipantsPaneOpen(getState());
if (navigator.product === 'ReactNative' || isParticipantsPaneVisible) {
return;
}
let notificationTitle;
let customActionNameKey;
let customActionHandler;
let descriptionKey;
let icon;
const knockingParticipants = getKnockingParticipants(getState());
const firstParticipant = knockingParticipants[0];
if (knockingParticipants.length > 1) {
descriptionKey = 'notify.participantsWantToJoin';
notificationTitle = i18n.t('notify.waitingParticipants', {
waitingParticipants: knockingParticipants.length
});
icon = NOTIFICATION_ICON.PARTICIPANTS;
customActionNameKey = [ 'notify.viewLobby' ];
customActionHandler = [ () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(openParticipantsPane());
}) ];
} else {
descriptionKey = 'notify.participantWantsToJoin';
notificationTitle = firstParticipant.name;
icon = NOTIFICATION_ICON.PARTICIPANT;
customActionNameKey = [ 'lobby.admit', 'lobby.reject' ];
customActionHandler = [ () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(approveKnockingParticipant(firstParticipant.id));
}),
() => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(rejectKnockingParticipant(firstParticipant.id));
}) ];
}
dispatch(showNotification({
title: notificationTitle,
descriptionKey,
uid: LOBBY_NOTIFICATION_ID,
customActionNameKey,
customActionHandler,
icon
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
if (typeof APP !== 'undefined') { if (typeof APP !== 'undefined') {
APP.API.notifyKnockingParticipant({ APP.API.notifyKnockingParticipant({
id, id,

View File

@ -5,6 +5,8 @@ import EditorErrorIcon from '@atlaskit/icon/glyph/editor/error';
import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info'; import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info';
import EditorSuccessIcon from '@atlaskit/icon/glyph/editor/success'; import EditorSuccessIcon from '@atlaskit/icon/glyph/editor/success';
import EditorWarningIcon from '@atlaskit/icon/glyph/editor/warning'; import EditorWarningIcon from '@atlaskit/icon/glyph/editor/warning';
import PeopleIcon from '@atlaskit/icon/glyph/people';
import PersonIcon from '@atlaskit/icon/glyph/person';
import QuestionsIcon from '@atlaskit/icon/glyph/questions'; import QuestionsIcon from '@atlaskit/icon/glyph/questions';
import React from 'react'; import React from 'react';
@ -171,6 +173,12 @@ class Notification extends AbstractNotification<Props> {
case NOTIFICATION_ICON.MESSAGE: case NOTIFICATION_ICON.MESSAGE:
Icon = QuestionsIcon; Icon = QuestionsIcon;
break; break;
case NOTIFICATION_ICON.PARTICIPANT:
Icon = PersonIcon;
break;
case NOTIFICATION_ICON.PARTICIPANTS:
Icon = PeopleIcon;
break;
default: default:
Icon = EditorInfoIcon; Icon = EditorInfoIcon;
break; break;

View File

@ -53,9 +53,18 @@ export const NOTIFICATION_TYPE_PRIORITIES = {
*/ */
export const NOTIFICATION_ICON = { export const NOTIFICATION_ICON = {
...NOTIFICATION_TYPE, ...NOTIFICATION_TYPE,
MESSAGE: 'message' MESSAGE: 'message',
PARTICIPANT: 'participant',
PARTICIPANTS: 'participants'
}; };
/**
* The identifier of the lobby notification.
*
* @type {string}
*/
export const LOBBY_NOTIFICATION_ID = 'LOBBY_NOTIFICATION';
/** /**
* The identifier of the raise hand notification. * The identifier of the raise hand notification.
* *