feat(notifications) Changed notifications stack to be full height

This is a stop-gap approach to remove the AtlasKit notifications stack.

Instead of using a AK FlagGroup to render our notifications (Flag components)
in, create our own container and use a fake FlagGroupContext provider, which is
what FlagGroup uses to control what flags can be dismissed.

Since we now render all notifications, the web part has been refactored to make
sure all notifications get a timer.

Added animations

Renamed DrawerPortal to JitsiPortal

Redesigned notifications
Changed notification text and icons color and added collared ribbon
This commit is contained in:
Saúl Ibarra Corretgé 2021-10-04 16:07:05 +02:00 committed by Saúl Ibarra Corretgé
parent b4f1ab991d
commit 0b984ce5f9
15 changed files with 468 additions and 283 deletions

View File

@ -5,6 +5,10 @@
bottom: 0; bottom: 0;
z-index: $drawerZ; z-index: $drawerZ;
border-radius: 16px 16px 0 0; border-radius: 16px 16px 0 0;
&.notification-portal {
z-index: $dropdownZ;
}
} }
.drawer-portal::after { .drawer-portal::after {

23
css/_notifications.scss Normal file
View File

@ -0,0 +1,23 @@
.notification-appear, .notification-enter {
opacity: 0;
position: relative;
left: -200px;
transition: all .2s !important; // !important needed to overwrite atlaskit default style
&-active {
opacity: 1;
left: 0;
}
}
.notification-exit {
opacity: 1;
position: relative;
left: 0;
transition: all .2s !important; // !important needed to overwrite atlaskit default style
&-active {
opacity: 0;
left: -200px;
}
}

View File

@ -106,5 +106,6 @@ $flagsImagePath: "../images/";
@import 'reactions-menu'; @import 'reactions-menu';
@import 'plan-limit'; @import 'plan-limit';
@import 'polls'; @import 'polls';
@import 'notifications';
/* Modules END */ /* Modules END */

View File

@ -2,7 +2,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Drawer, DrawerPortal, DialogPortal } from '../../../toolbox/components/web'; import { Drawer, JitsiPortal, DialogPortal } from '../../../toolbox/components/web';
import { isMobileBrowser } from '../../environment/utils'; import { isMobileBrowser } from '../../environment/utils';
import { getContextMenuStyle } from '../functions.web'; import { getContextMenuStyle } from '../functions.web';
@ -173,13 +173,13 @@ class Popover extends Component<Props, State> {
id = { id } id = { id }
onClick = { this._onShowDialog }> onClick = { this._onShowDialog }>
{ children } { children }
<DrawerPortal> <JitsiPortal>
<Drawer <Drawer
isOpen = { this.state.showDialog } isOpen = { this.state.showDialog }
onClose = { this._onHideDialog }> onClose = { this._onHideDialog }>
{ content } { content }
</Drawer> </Drawer>
</DrawerPortal> </JitsiPortal>
</div> </div>
); );
} }

View File

@ -19,7 +19,7 @@ import { ParticipantsPane } from '../../../participants-pane/components/web';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions'; import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import { Prejoin, isPrejoinPageVisible, isPrejoinPageLoading } from '../../../prejoin'; import { Prejoin, isPrejoinPageVisible, isPrejoinPageLoading } from '../../../prejoin';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web'; import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
import { Toolbox } from '../../../toolbox/components/web'; import { JitsiPortal, Toolbox } from '../../../toolbox/components/web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout'; import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { maybeShowSuboptimalExperienceNotification } from '../../functions'; import { maybeShowSuboptimalExperienceNotification } from '../../functions';
import { import {
@ -86,6 +86,11 @@ type Props = AbstractProps & {
*/ */
_mouseMoveCallbackInterval: number, _mouseMoveCallbackInterval: number,
/**
*Whether or not the notifications should be displayed in the overflow drawer.
*/
_overflowDrawer: boolean,
/** /**
* Name for this conference room. * Name for this conference room.
*/ */
@ -209,6 +214,8 @@ class Conference extends AbstractConference<Props, *> {
const { const {
_isParticipantsPaneVisible, _isParticipantsPaneVisible,
_layoutClassName, _layoutClassName,
_notificationsVisible,
_overflowDrawer,
_showLobby, _showLobby,
_showPrejoin _showPrejoin
} = this.props; } = this.props;
@ -239,7 +246,12 @@ class Conference extends AbstractConference<Props, *> {
{ _showPrejoin || _showLobby || <Toolbox showDominantSpeakerName = { true } /> } { _showPrejoin || _showLobby || <Toolbox showDominantSpeakerName = { true } /> }
<Chat /> <Chat />
{ this.renderNotificationsContainer() } {_notificationsVisible && (_overflowDrawer
? <JitsiPortal className = 'notification-portal'>
{this.renderNotificationsContainer({ portal: true })}
</JitsiPortal>
: this.renderNotificationsContainer())
}
<CalleeInfoContainer /> <CalleeInfoContainer />
@ -368,6 +380,7 @@ class Conference extends AbstractConference<Props, *> {
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state) {
const { backgroundAlpha, mouseMoveCallbackInterval } = state['features/base/config']; const { backgroundAlpha, mouseMoveCallbackInterval } = state['features/base/config'];
const { overflowDrawer } = state['features/toolbox'];
return { return {
...abstractMapStateToProps(state), ...abstractMapStateToProps(state),
@ -375,6 +388,7 @@ function _mapStateToProps(state) {
_isParticipantsPaneVisible: getParticipantsPaneOpen(state), _isParticipantsPaneVisible: getParticipantsPaneOpen(state),
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)], _layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
_mouseMoveCallbackInterval: mouseMoveCallbackInterval, _mouseMoveCallbackInterval: mouseMoveCallbackInterval,
_overflowDrawer: overflowDrawer,
_roomName: getConferenceNameForTitle(state), _roomName: getConferenceNameForTitle(state),
_showLobby: getIsLobbyVisible(state), _showLobby: getIsLobbyVisible(state),
_showPrejoin: isPrejoinPageVisible(state) || isPrejoinPageLoading(state) _showPrejoin: isPrejoinPageVisible(state) || isPrejoinPageLoading(state)

View File

@ -1,188 +0,0 @@
// @flow
import { Component } from 'react';
import { hideNotification } from '../actions';
import { areThereNotifications } from '../functions';
export type Props = {
/**
* The notifications to be displayed, with the first index being the
* notification at the top and the rest shown below it in order.
*/
_notifications: Array<Object>,
/**
* The length, in milliseconds, to use as a default timeout for all
* dismissable timeouts that do not have a timeout specified.
*/
autoDismissTimeout: number,
/**
* Invoked to update the redux store in order to remove notifications.
*/
dispatch: Function
};
declare var interfaceConfig: Object;
/**
* Abstract class for {@code NotificationsContainer} component.
*/
export default class AbstractNotificationsContainer<P: Props>
extends Component<P> {
/**
* A timeout id returned by setTimeout.
*/
_notificationDismissTimeout: ?TimeoutID;
/**
* Initializes a new {@code AbstractNotificationsContainer} instance.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
/**
* The timeout set for automatically dismissing a displayed
* notification. This value is set on the instance and not state to
* avoid additional re-renders.
*
* @type {number|null}
*/
this._notificationDismissTimeout = null;
// Bind event handlers so they are only bound once for every instance.
this._onDismissed = this._onDismissed.bind(this);
}
/**
* Sets a timeout for the first notification (if applicable).
*
* @inheritdoc
*/
componentDidMount() {
// Set the initial dismiss timeout (if any)
this._manageDismissTimeout();
}
/**
* Sets a timeout if the currently displayed notification has changed.
*
* @inheritdoc
*/
componentDidUpdate(prevProps: P) {
this._manageDismissTimeout(prevProps);
}
/**
* Sets/clears the dismiss timeout for the top notification.
*
* @param {P} [prevProps] - The previous properties (if called from
* {@code componentDidUpdate}).
* @returns {void}
* @private
*/
_manageDismissTimeout(prevProps: ?P) {
const { _notifications, autoDismissTimeout } = this.props;
if (_notifications.length) {
const notification = _notifications[0];
const previousNotification
= prevProps && prevProps._notifications.length
? prevProps._notifications[0]
: undefined;
if (notification !== previousNotification) {
this._clearNotificationDismissTimeout();
if (notification
&& (notification.timeout
|| typeof autoDismissTimeout === 'number')
&& notification.props.isDismissAllowed !== false) {
const {
timeout = autoDismissTimeout,
uid
} = notification;
this._notificationDismissTimeout = setTimeout(() => {
// Perform a no-op if a timeout is not specified.
this._onDismissed(uid);
}, timeout);
}
}
} else if (this._notificationDismissTimeout) {
// Clear timeout when all notifications are cleared (e.g external
// call to clear them)
this._clearNotificationDismissTimeout();
}
}
/**
* Clear any dismissal timeout that is still active.
*
* @inheritdoc
* returns {void}
*/
componentWillUnmount() {
this._clearNotificationDismissTimeout();
}
_onDismissed: number => void;
/**
* Clears the running notification dismiss timeout, if any.
*
* @returns {void}
*/
_clearNotificationDismissTimeout() {
this._notificationDismissTimeout
&& clearTimeout(this._notificationDismissTimeout);
this._notificationDismissTimeout = null;
}
/**
* Emits an action to remove the notification from the redux store so it
* stops displaying.
*
* @param {number} uid - The id of the notification to be removed.
* @private
* @returns {void}
*/
_onDismissed(uid) {
const { _notifications } = this.props;
// Clear the timeout only if it's the top notification that's being
// dismissed (the timeout is set only for the top one).
if (!_notifications.length || _notifications[0].uid === uid) {
this._clearNotificationDismissTimeout();
}
this.props.dispatch(hideNotification(uid));
}
}
/**
* Maps (parts of) the Redux state to the associated NotificationsContainer's
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _notifications: Array
* }}
*/
export function _abstractMapStateToProps(state: Object) {
const { notifications } = state['features/notifications'];
const _visible = areThereNotifications(state);
return {
_notifications: _visible ? notifications : [],
autoDismissTimeout: typeof interfaceConfig === 'undefined'
? undefined // Ignore for the case of mobile
: interfaceConfig.ENFORCE_NOTIFICATION_AUTO_DISMISS_TIMEOUT
};
}

View File

@ -1,18 +1,27 @@
// @flow // @flow
import React from 'react'; import React, { Component } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import AbstractNotificationsContainer, { import { hideNotification } from '../../actions';
_abstractMapStateToProps, import { areThereNotifications } from '../../functions';
type Props as AbstractProps
} from '../AbstractNotificationsContainer';
import Notification from './Notification'; import Notification from './Notification';
import styles from './styles'; import styles from './styles';
type Props = AbstractProps & { type Props = {
/**
* The notifications to be displayed, with the first index being the
* notification at the top and the rest shown below it in order.
*/
_notifications: Array<Object>,
/**
* Invoked to update the redux store in order to remove notifications.
*/
dispatch: Function,
/** /**
* Any custom styling applied to the notifications container. * Any custom styling applied to the notifications container.
@ -27,8 +36,134 @@ type Props = AbstractProps & {
* *
* @extends {Component} * @extends {Component}
*/ */
class NotificationsContainer class NotificationsContainer extends Component<Props> {
extends AbstractNotificationsContainer<Props> {
/**
* A timeout id returned by setTimeout.
*/
_notificationDismissTimeout: ?TimeoutID;
/**
* Initializes a new {@code NotificationsContainer} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
/**
* The timeout set for automatically dismissing a displayed
* notification. This value is set on the instance and not state to
* avoid additional re-renders.
*
* @type {number|null}
*/
this._notificationDismissTimeout = null;
// Bind event handlers so they are only bound once for every instance.
this._onDismissed = this._onDismissed.bind(this);
}
/**
* Sets a timeout for the first notification (if applicable).
*
* @inheritdoc
*/
componentDidMount() {
// Set the initial dismiss timeout (if any)
this._manageDismissTimeout();
}
/**
* Sets a timeout if the currently displayed notification has changed.
*
* @inheritdoc
*/
componentDidUpdate(prevProps: Props) {
this._manageDismissTimeout(prevProps);
}
/**
* Sets/clears the dismiss timeout for the top notification.
*
* @param {P} [prevProps] - The previous properties (if called from
* {@code componentDidUpdate}).
* @returns {void}
* @private
*/
_manageDismissTimeout(prevProps: ?Props) {
const { _notifications } = this.props;
if (_notifications.length) {
const notification = _notifications[0];
const previousNotification
= prevProps && prevProps._notifications.length
? prevProps._notifications[0]
: undefined;
if (notification !== previousNotification) {
this._clearNotificationDismissTimeout();
if (notification && notification.timeout && notification.props.isDismissAllowed !== false) {
const {
timeout,
uid
} = notification;
this._notificationDismissTimeout = setTimeout(() => {
// Perform a no-op if a timeout is not specified.
this._onDismissed(uid);
}, timeout);
}
}
} else if (this._notificationDismissTimeout) {
// Clear timeout when all notifications are cleared (e.g external
// call to clear them)
this._clearNotificationDismissTimeout();
}
}
/**
* Clear any dismissal timeout that is still active.
*
* @inheritdoc
*/
componentWillUnmount() {
this._clearNotificationDismissTimeout();
}
/**
* Clears the running notification dismiss timeout, if any.
*
* @returns {void}
*/
_clearNotificationDismissTimeout() {
this._notificationDismissTimeout && clearTimeout(this._notificationDismissTimeout);
this._notificationDismissTimeout = null;
}
_onDismissed: number => void;
/**
* Emits an action to remove the notification from the redux store so it
* stops displaying.
*
* @param {number} uid - The id of the notification to be removed.
* @private
* @returns {void}
*/
_onDismissed(uid) {
const { _notifications } = this.props;
// Clear the timeout only if it's the top notification that's being
// dismissed (the timeout is set only for the top one).
if (!_notifications.length || _notifications[0].uid === uid) {
this._clearNotificationDismissTimeout();
}
this.props.dispatch(hideNotification(uid));
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
@ -39,8 +174,7 @@ class NotificationsContainer
const { _notifications } = this.props; const { _notifications } = this.props;
// Currently the native container displays only the topmost notification // Currently the native container displays only the topmost notification
const theNotification const theNotification = _notifications[0];
= _notifications && _notifications.length && _notifications[0];
if (!theNotification) { if (!theNotification) {
return null; return null;
@ -64,4 +198,21 @@ class NotificationsContainer
_onDismissed: number => void; _onDismissed: number => void;
} }
export default connect(_abstractMapStateToProps)(NotificationsContainer); /**
* Maps (parts of) the Redux state to the associated NotificationsContainer's
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
export function mapStateToProps(state: Object) {
const { notifications } = state['features/notifications'];
const _visible = areThereNotifications(state);
return {
_notifications: _visible ? notifications : []
};
}
export default connect(mapStateToProps)(NotificationsContainer);

View File

@ -2,12 +2,10 @@
import Flag from '@atlaskit/flag'; import Flag from '@atlaskit/flag';
import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info'; import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info';
import ErrorIcon from '@atlaskit/icon/glyph/error';
import WarningIcon from '@atlaskit/icon/glyph/warning';
import { colors } from '@atlaskit/theme';
import React from 'react'; import React from 'react';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { colors } from '../../../base/ui/Tokens';
import { NOTIFICATION_TYPE } from '../../constants'; import { NOTIFICATION_TYPE } from '../../constants';
import AbstractNotification, { import AbstractNotification, {
type Props type Props
@ -21,11 +19,9 @@ declare var interfaceConfig: Object;
* @type {{error, info, normal, success, warning}} * @type {{error, info, normal, success, warning}}
*/ */
const ICON_COLOR = { const ICON_COLOR = {
error: colors.R400, error: colors.error06,
info: colors.N500, normal: colors.primary06,
normal: colors.N0, warning: colors.warning05
success: colors.G400,
warning: colors.Y200
}; };
/** /**
@ -42,7 +38,6 @@ class Notification extends AbstractNotification<Props> {
*/ */
render() { render() {
const { const {
appearance,
hideErrorSupportLink, hideErrorSupportLink,
t, t,
title, title,
@ -54,7 +49,6 @@ class Notification extends AbstractNotification<Props> {
return ( return (
<Flag <Flag
actions = { this._mapAppearanceToButtons(hideErrorSupportLink) } actions = { this._mapAppearanceToButtons(hideErrorSupportLink) }
appearance = { appearance }
description = { this._renderDescription() } description = { this._renderDescription() }
icon = { this._mapAppearanceToIcon() } icon = { this._mapAppearanceToIcon() }
id = { uid } id = { uid }
@ -163,31 +157,13 @@ class Notification extends AbstractNotification<Props> {
const secIconColor = ICON_COLOR[this.props.appearance]; const secIconColor = ICON_COLOR[this.props.appearance];
const iconSize = 'medium'; const iconSize = 'medium';
switch (appearance) { return <>
case NOTIFICATION_TYPE.ERROR: <div className = { `ribbon ${appearance}` } />
return ( <EditorInfoIcon
<ErrorIcon label = { appearance }
label = { appearance } secondaryColor = { secIconColor }
secondaryColor = { secIconColor } size = { iconSize } />
size = { iconSize } /> </>;
);
case NOTIFICATION_TYPE.WARNING:
return (
<WarningIcon
label = { appearance }
secondaryColor = { secIconColor }
size = { iconSize } />
);
default:
return (
<EditorInfoIcon
label = { appearance }
secondaryColor = { secIconColor }
size = { iconSize } />
);
}
} }
} }

View File

@ -1,38 +1,173 @@
// @flow // @flow
import { FlagGroup } from '@atlaskit/flag'; import { FlagGroupContext } from '@atlaskit/flag/flag-group';
import React from 'react'; import { AtlasKitThemeProvider } from '@atlaskit/theme';
import { withStyles } from '@material-ui/styles';
import React, { Component } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import AbstractNotificationsContainer, { import { hideNotification } from '../../actions';
_abstractMapStateToProps, import { areThereNotifications } from '../../functions';
type Props as AbstractProps
} from '../AbstractNotificationsContainer';
import Notification from './Notification'; import Notification from './Notification';
type Props = AbstractProps & { declare var interfaceConfig: Object;
type Props = {
/** /**
* Whether we are a SIP gateway or not. * Whether we are a SIP gateway or not.
*/ */
_iAmSipGateway: boolean, _iAmSipGateway: boolean,
/** /**
* Whether or not the chat is open.
*/
_isChatOpen: boolean,
/**
* The notifications to be displayed, with the first index being the
* notification at the top and the rest shown below it in order.
*/
_notifications: Array<Object>,
/**
* The length, in milliseconds, to use as a default timeout for all
* dismissible timeouts that do not have a timeout specified.
*/
autoDismissTimeout: number,
/**
* JSS classes object.
*/
classes: Object,
/**
* Invoked to update the redux store in order to remove notifications.
*/
dispatch: Function,
/**
* Whether or not the notifications are displayed in a portal.
*/
portal?: boolean,
/**
* Invoked to obtain translated strings. * Invoked to obtain translated strings.
*/ */
t: Function t: Function
};
const useStyles = theme => {
return {
container: {
position: 'absolute',
left: '16px',
bottom: '90px',
width: '400px',
maxWidth: '100%',
zIndex: 600
},
containerPortal: {
maxWidth: 'calc(100% - 32px)'
},
containerChatOpen: {
left: '331px'
},
transitionGroup: {
'& > *': {
marginBottom: '20px',
borderRadius: '6px!important', // !important used to overwrite atlaskit style
position: 'relative'
},
'& div > span > svg > path': {
fill: 'inherit'
},
'& div > span, & div > p': {
color: theme.palette.field01
},
'& .ribbon': {
width: '4px',
height: 'calc(100% - 16px)',
position: 'absolute',
left: 0,
top: '8px',
borderRadius: '4px',
'&.normal': {
backgroundColor: theme.palette.link01Active
},
'&.error': {
backgroundColor: theme.palette.iconError
},
'&.warning': {
backgroundColor: theme.palette.warning01
}
}
}
};
}; };
/** /**
* Implements a React {@link Component} which displays notifications and handles * Implements a React {@link Component} which displays notifications and handles
* automatic dismissmal after a notification is shown for a defined timeout * automatic dismissal after a notification is shown for a defined timeout
* period. * period.
* *
* @extends {Component} * @extends {Component}
*/ */
class NotificationsContainer extends AbstractNotificationsContainer<Props> { class NotificationsContainer extends Component<Props> {
_api: Object;
_timeouts: Map<string, TimeoutID>;
/**
* Initializes a new {@code NotificationsContainer} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._timeouts = new Map();
// Bind event handlers so they are only bound once for every instance.
this._onDismissed = this._onDismissed.bind(this);
// HACK ALERT! We are rendering AtlasKit Flag elements outside of a FlagGroup container.
// In order to hook-up the dismiss action we'll a fake context provider,
// just like FlagGroup does.
this._api = {
onDismissed: this._onDismissed,
dismissAllowed: () => true
};
}
/**
* Sets a timeout for each notification, where applicable.
*
* @inheritdoc
*/
componentDidMount() {
this._updateTimeouts();
}
/**
* Sets a timeout for each notification, where applicable.
*
* @inheritdoc
*/
componentDidUpdate() {
this._updateTimeouts();
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
@ -46,17 +181,46 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
} }
return ( return (
<FlagGroup <AtlasKitThemeProvider mode = 'light'>
id = 'notifications-container' <FlagGroupContext.Provider value = { this._api }>
label = { this.props.t('notify.groupTitle') } <div
onDismissed = { this._onDismissed }> className = { `${this.props.classes.container} ${this.props.portal
{ this._renderFlags() } ? this.props.classes.containerPortal
</FlagGroup> : this.props._isChatOpen
? this.props.classes.containerChatOpen
: ''}`
}
id = 'notifications-container'>
<TransitionGroup className = { this.props.classes.transitionGroup }>
{this._renderFlags()}
</TransitionGroup>
</div>
</FlagGroupContext.Provider>
</AtlasKitThemeProvider>
); );
} }
_onDismissed: number => void; _onDismissed: number => void;
/**
* Emits an action to remove the notification from the redux store so it
* stops displaying.
*
* @param {number} uid - The id of the notification to be removed.
* @private
* @returns {void}
*/
_onDismissed(uid) {
const timeout = this._timeouts.get(uid);
if (timeout) {
clearTimeout(timeout);
this._timeouts.delete(uid);
}
this.props.dispatch(hideNotification(uid));
}
/** /**
* Renders notifications to display as ReactElements. An empty array will * Renders notifications to display as ReactElements. An empty array will
* be returned if notifications are disabled. * be returned if notifications are disabled.
@ -74,15 +238,46 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
// either id or key to set a key on notifications, but accessing // either id or key to set a key on notifications, but accessing
// props.key will cause React to print an error. // props.key will cause React to print an error.
return ( return (
<Notification <CSSTransition
{ ...props } appear = { true }
id = { uid } classNames = 'notification'
in = { true }
key = { uid } key = { uid }
onDismissed = { this._onDismissed } timeout = { 200 }>
uid = { uid } /> <Notification
{ ...props }
id = { uid }
onDismissed = { this._onDismissed }
uid = { uid } />
</CSSTransition>
); );
}); });
} }
/**
* Updates the timeouts for every notification.
*
* @returns {void}
*/
_updateTimeouts() {
const { _notifications, autoDismissTimeout } = this.props;
for (const notification of _notifications) {
if ((notification.timeout || typeof autoDismissTimeout === 'number')
&& notification.props.isDismissAllowed !== false
&& !this._timeouts.has(notification.uid)) {
const {
timeout = autoDismissTimeout,
uid
} = notification;
const timerID = setTimeout(() => {
this._onDismissed(uid);
}, timeout);
this._timeouts.set(uid, timerID);
}
}
}
} }
/** /**
@ -93,13 +288,17 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
* @returns {Props} * @returns {Props}
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state) {
const { notifications } = state['features/notifications'];
const { iAmSipGateway } = state['features/base/config']; const { iAmSipGateway } = state['features/base/config'];
const { isOpen: isChatOpen } = state['features/chat'];
const _visible = areThereNotifications(state);
return { return {
..._abstractMapStateToProps(state), _iAmSipGateway: Boolean(iAmSipGateway),
_iAmSipGateway: Boolean(iAmSipGateway) _isChatOpen: isChatOpen,
_notifications: _visible ? notifications : [],
autoDismissTimeout: interfaceConfig.ENFORCE_NOTIFICATION_AUTO_DISMISS_TIMEOUT
}; };
} }
export default translate(connect(_mapStateToProps)(withStyles(useStyles)(NotificationsContainer)));
export default translate(connect(_mapStateToProps)(NotificationsContainer));

View File

@ -10,7 +10,7 @@ import { Icon, IconCheck, IconClose } from '../../../base/icons';
import { withPixelLineHeight } from '../../../base/styles/functions.web'; import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { admitMultiple } from '../../../lobby/actions.web'; import { admitMultiple } from '../../../lobby/actions.web';
import { getLobbyEnabled, getKnockingParticipants } from '../../../lobby/functions'; import { getLobbyEnabled, getKnockingParticipants } from '../../../lobby/functions';
import { Drawer, DrawerPortal } from '../../../toolbox/components/web'; import { Drawer, JitsiPortal } from '../../../toolbox/components/web';
import { showOverflowDrawer } from '../../../toolbox/functions'; import { showOverflowDrawer } from '../../../toolbox/functions';
import { useLobbyActions, useParticipantDrawer } from '../../hooks'; import { useLobbyActions, useParticipantDrawer } from '../../hooks';
@ -96,7 +96,7 @@ export default function LobbyParticipants() {
openDrawerForParticipant = { openDrawerForParticipant } openDrawerForParticipant = { openDrawerForParticipant }
overflowDrawer = { overflowDrawer } overflowDrawer = { overflowDrawer }
participants = { participants } /> participants = { participants } />
<DrawerPortal> <JitsiPortal>
<Drawer <Drawer
isOpen = { Boolean(drawerParticipant && overflowDrawer) } isOpen = { Boolean(drawerParticipant && overflowDrawer) }
onClose = { closeDrawer }> onClose = { closeDrawer }>
@ -128,7 +128,7 @@ export default function LobbyParticipants() {
</li> </li>
</ul> </ul>
</Drawer> </Drawer>
</DrawerPortal> </JitsiPortal>
</> </>
); );
} }

View File

@ -30,7 +30,7 @@ import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks'; import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
import { openChatById } from '../../../chat/actions'; import { openChatById } from '../../../chat/actions';
import { setVolume } from '../../../filmstrip/actions.web'; import { setVolume } from '../../../filmstrip/actions.web';
import { Drawer, DrawerPortal } from '../../../toolbox/components/web'; import { Drawer, JitsiPortal } from '../../../toolbox/components/web';
import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu'; import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
import { VolumeSlider } from '../../../video-menu/components/web'; import { VolumeSlider } from '../../../video-menu/components/web';
import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog'; import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
@ -533,7 +533,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
{ actions } { actions }
</ContextMenu>} </ContextMenu>}
<DrawerPortal> <JitsiPortal>
<Drawer <Drawer
isOpen = { drawerParticipant && overflowDrawer } isOpen = { drawerParticipant && overflowDrawer }
onClose = { closeDrawer }> onClose = { closeDrawer }>
@ -549,7 +549,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
{ actions } { actions }
</div> </div>
</Drawer> </Drawer>
</DrawerPortal> </JitsiPortal>
</> </>
); );
} }

View File

@ -7,7 +7,7 @@ import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants'; import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { Drawer, DrawerPortal } from '../../../toolbox/components/web'; import { Drawer, JitsiPortal } from '../../../toolbox/components/web';
import { showOverflowDrawer } from '../../../toolbox/functions'; import { showOverflowDrawer } from '../../../toolbox/functions';
import { MuteEveryoneDialog } from '../../../video-menu/components/'; import { MuteEveryoneDialog } from '../../../video-menu/components/';
import { close } from '../../actions'; import { close } from '../../actions';
@ -166,13 +166,13 @@ class ParticipantsPane extends Component<Props, State> {
</Footer> </Footer>
)} )}
</div> </div>
<DrawerPortal> <JitsiPortal>
<Drawer <Drawer
isOpen = { contextOpen && _overflowDrawer } isOpen = { contextOpen && _overflowDrawer }
onClose = { this._onDrawerClose }> onClose = { this._onDrawerClose }>
<FooterContextMenu inDrawer = { true } /> <FooterContextMenu inDrawer = { true } />
</Drawer> </Drawer>
</DrawerPortal> </JitsiPortal>
</div> </div>
</ThemeProvider> </ThemeProvider>
); );

View File

@ -8,7 +8,12 @@ type Props = {
/** /**
* The component(s) to be displayed within the drawer portal. * The component(s) to be displayed within the drawer portal.
*/ */
children: React$Node children: React$Node,
/**
* Class name used to add custom styles to the portal.
*/
className?: string
}; };
/** /**
@ -17,12 +22,12 @@ type Props = {
* *
* @returns {ReactElement} * @returns {ReactElement}
*/ */
function DrawerPortal({ children }: Props) { function JitsiPortal({ children, className }: Props) {
return ( return (
<DialogPortal className = 'drawer-portal'> <DialogPortal className = { `drawer-portal ${className ?? ''}` }>
{ children } { children }
</DialogPortal> </DialogPortal>
); );
} }
export default DrawerPortal; export default JitsiPortal;

View File

@ -12,7 +12,7 @@ import { type ReactionEmojiProps } from '../../../reactions/constants';
import { getReactionsQueue } from '../../../reactions/functions.any'; import { getReactionsQueue } from '../../../reactions/functions.any';
import Drawer from './Drawer'; import Drawer from './Drawer';
import DrawerPortal from './DrawerPortal'; import JitsiPortal from './JitsiPortal';
import ToolbarButton from './ToolbarButton'; import ToolbarButton from './ToolbarButton';
/** /**
@ -114,7 +114,7 @@ class OverflowMenuButton extends Component<Props> {
overflowDrawer ? ( overflowDrawer ? (
<> <>
{this._renderToolbarButton()} {this._renderToolbarButton()}
<DrawerPortal> <JitsiPortal>
<Drawer <Drawer
isOpen = { isOpen } isOpen = { isOpen }
onClose = { this._onCloseDialog }> onClose = { this._onCloseDialog }>
@ -128,7 +128,7 @@ class OverflowMenuButton extends Component<Props> {
reaction = { reaction } reaction = { reaction }
uid = { uid } />))} uid = { uid } />))}
</div>} </div>}
</DrawerPortal> </JitsiPortal>
</> </>
) : ( ) : (
<InlineDialog <InlineDialog

View File

@ -3,5 +3,5 @@ export { default as VideoSettingsButton } from './VideoSettingsButton';
export { default as ToolbarButton } from './ToolbarButton'; export { default as ToolbarButton } from './ToolbarButton';
export { default as Toolbox } from './Toolbox'; export { default as Toolbox } from './Toolbox';
export { default as Drawer } from './Drawer'; export { default as Drawer } from './Drawer';
export { default as DrawerPortal } from './DrawerPortal'; export { default as JitsiPortal } from './JitsiPortal';
export { default as DialogPortal } from './DialogPortal'; export { default as DialogPortal } from './DialogPortal';