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 {
list-style-type: none;
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 ",
"oldElectronClientDescription2": "latest build",
"oldElectronClientDescription3": " now!",
"participantWantsToJoin": "Wants to join the meeting",
"participantsWantToJoin": "Want to join the meeting",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
"raiseHandAction": "Raise hand",
@ -661,7 +663,9 @@
"videoMutedRemotelyDescription": "You can always turn it on again.",
"videoMutedRemotelyTitle": "Your video has been turned off by {{participantDisplayName}}",
"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": {
"actions": {

View File

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

View File

@ -14,10 +14,9 @@ import { Chat } from '../../../chat';
import { Filmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
import { LobbyScreen } from '../../../lobby';
import { getIsLobbyVisible } from '../../../lobby/functions';
import { ParticipantsPane } from '../../../participants-pane/components/web';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import { toggleToolboxVisible } from '../../../toolbox/actions.any';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
@ -72,11 +71,6 @@ type Props = AbstractProps & {
*/
_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
* application layout.
@ -216,7 +210,6 @@ class Conference extends AbstractConference<Props, *> {
*/
render() {
const {
_isParticipantsPaneVisible,
_layoutClassName,
_notificationsVisible,
_overflowDrawer,
@ -242,10 +235,6 @@ class Conference extends AbstractConference<Props, *> {
id = 'videospace'
onTouchStart = { this._onVidespaceTouchStart }>
<LargeVideo />
{!_isParticipantsPaneVisible
&& <div id = 'notification-participant-list'>
<KnockingParticipantList />
</div>}
<Filmstrip />
</div>
@ -401,7 +390,6 @@ function _mapStateToProps(state) {
return {
...abstractMapStateToProps(state),
_backgroundAlpha: backgroundAlpha,
_isParticipantsPaneVisible: getParticipantsPaneOpen(state),
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
_mouseMoveCallbackInterval: mouseMoveCallbackInterval,
_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
import React from 'react';
import React, { PureComponent } from 'react';
import { ScrollView, Text, View, TouchableOpacity } from 'react-native';
import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { setKnockingParticipantApproval } from '../../actions';
import { HIDDEN_EMAILS } from '../../constants';
import AbstractKnockingParticipantList, {
mapStateToProps as abstractMapStateToProps,
type Props
} from '../AbstractKnockingParticipantList';
import { getKnockingParticipants, getLobbyEnabled } from '../../functions';
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.
*/
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}.
*
@ -78,6 +115,19 @@ class KnockingParticipantList extends AbstractKnockingParticipantList {
}
_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.
* @returns {Props}
*/
function _mapStateToProps(state: Object): $Shape<Props> {
const abstractProps = abstractMapStateToProps(state);
function _mapStateToProps(state): Object {
const lobbyEnabled = getLobbyEnabled(state);
const knockingParticipants = getKnockingParticipants(state);
return {
...abstractProps,
_visible: lobbyEnabled && isLocalParticipantModerator(state),
// 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
export { default as KnockingParticipantList } from './KnockingParticipantList';
export { default as LobbySection } from './LobbySection';
export { default as LobbyScreen } from './LobbyScreen';

View File

@ -1,5 +1,6 @@
// @flow
import i18n from 'i18next';
import { batch } from 'react-redux';
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 { playSound, registerSound, unregisterSound } from '../base/sounds';
import { isTestModeEnabled } from '../base/testing';
import { approveKnockingParticipant, rejectKnockingParticipant } from '../lobby/actions';
import {
LOBBY_NOTIFICATION_ID,
NOTIFICATION_ICON,
NOTIFICATION_TIMEOUT_TYPE,
NOTIFICATION_TYPE,
hideNotification,
showNotification
} from '../notifications';
import { open as openParticipantsPane } from '../participants-pane/actions';
import { getParticipantsPaneOpen } from '../participants-pane/functions';
import { shouldAutoKnock } from '../prejoin/functions';
import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED } from './actionTypes';
@ -31,6 +38,7 @@ import {
setPasswordJoinFailed
} from './actions';
import { KNOCKING_PARTICIPANT_SOUND_ID } from './constants';
import { getKnockingParticipants } from './functions';
import { KNOCKING_PARTICIPANT_FILE } from './sounds';
declare var APP: Object;
@ -81,6 +89,55 @@ StateListenerRegistry.register(
})
);
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') {
APP.API.notifyKnockingParticipant({
id,

View File

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

View File

@ -53,9 +53,18 @@ export const NOTIFICATION_TYPE_PRIORITIES = {
*/
export const NOTIFICATION_ICON = {
...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.
*