Browse Source

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
master
Saúl Ibarra Corretgé 3 years ago
parent
commit
0b984ce5f9

+ 4
- 0
css/_drawer.scss View File

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

+ 23
- 0
css/_notifications.scss View File

@@ -0,0 +1,23 @@
1
+.notification-appear, .notification-enter {
2
+    opacity: 0;
3
+    position: relative;
4
+    left: -200px;
5
+    transition: all .2s !important; // !important needed to overwrite atlaskit default style
6
+
7
+    &-active {
8
+        opacity: 1;
9
+        left: 0;
10
+    }
11
+}
12
+
13
+.notification-exit {
14
+    opacity: 1;
15
+    position: relative;
16
+    left: 0;
17
+    transition: all .2s !important; // !important needed to overwrite atlaskit default style
18
+
19
+    &-active {
20
+        opacity: 0;
21
+        left: -200px;
22
+    }
23
+}

+ 1
- 0
css/main.scss View File

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

+ 3
- 3
react/features/base/popover/components/Popover.web.js View File

@@ -2,7 +2,7 @@
2 2
 
3 3
 import React, { Component } from 'react';
4 4
 
5
-import { Drawer, DrawerPortal, DialogPortal } from '../../../toolbox/components/web';
5
+import { Drawer, JitsiPortal, DialogPortal } from '../../../toolbox/components/web';
6 6
 import { isMobileBrowser } from '../../environment/utils';
7 7
 import { getContextMenuStyle } from '../functions.web';
8 8
 
@@ -173,13 +173,13 @@ class Popover extends Component<Props, State> {
173 173
                     id = { id }
174 174
                     onClick = { this._onShowDialog }>
175 175
                     { children }
176
-                    <DrawerPortal>
176
+                    <JitsiPortal>
177 177
                         <Drawer
178 178
                             isOpen = { this.state.showDialog }
179 179
                             onClose = { this._onHideDialog }>
180 180
                             { content }
181 181
                         </Drawer>
182
-                    </DrawerPortal>
182
+                    </JitsiPortal>
183 183
                 </div>
184 184
             );
185 185
         }

+ 16
- 2
react/features/conference/components/web/Conference.js View File

@@ -19,7 +19,7 @@ import { ParticipantsPane } from '../../../participants-pane/components/web';
19 19
 import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
20 20
 import { Prejoin, isPrejoinPageVisible, isPrejoinPageLoading } from '../../../prejoin';
21 21
 import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
22
-import { Toolbox } from '../../../toolbox/components/web';
22
+import { JitsiPortal, Toolbox } from '../../../toolbox/components/web';
23 23
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
24 24
 import { maybeShowSuboptimalExperienceNotification } from '../../functions';
25 25
 import {
@@ -86,6 +86,11 @@ type Props = AbstractProps & {
86 86
      */
87 87
     _mouseMoveCallbackInterval: number,
88 88
 
89
+    /**
90
+     *Whether or not the notifications should be displayed in the overflow drawer.
91
+     */
92
+    _overflowDrawer: boolean,
93
+
89 94
     /**
90 95
      * Name for this conference room.
91 96
      */
@@ -209,6 +214,8 @@ class Conference extends AbstractConference<Props, *> {
209 214
         const {
210 215
             _isParticipantsPaneVisible,
211 216
             _layoutClassName,
217
+            _notificationsVisible,
218
+            _overflowDrawer,
212 219
             _showLobby,
213 220
             _showPrejoin
214 221
         } = this.props;
@@ -239,7 +246,12 @@ class Conference extends AbstractConference<Props, *> {
239 246
                     { _showPrejoin || _showLobby || <Toolbox showDominantSpeakerName = { true } /> }
240 247
                     <Chat />
241 248
 
242
-                    { this.renderNotificationsContainer() }
249
+                    {_notificationsVisible && (_overflowDrawer
250
+                        ? <JitsiPortal className = 'notification-portal'>
251
+                            {this.renderNotificationsContainer({ portal: true })}
252
+                        </JitsiPortal>
253
+                        : this.renderNotificationsContainer())
254
+                    }
243 255
 
244 256
                     <CalleeInfoContainer />
245 257
 
@@ -368,6 +380,7 @@ class Conference extends AbstractConference<Props, *> {
368 380
  */
369 381
 function _mapStateToProps(state) {
370 382
     const { backgroundAlpha, mouseMoveCallbackInterval } = state['features/base/config'];
383
+    const { overflowDrawer } = state['features/toolbox'];
371 384
 
372 385
     return {
373 386
         ...abstractMapStateToProps(state),
@@ -375,6 +388,7 @@ function _mapStateToProps(state) {
375 388
         _isParticipantsPaneVisible: getParticipantsPaneOpen(state),
376 389
         _layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
377 390
         _mouseMoveCallbackInterval: mouseMoveCallbackInterval,
391
+        _overflowDrawer: overflowDrawer,
378 392
         _roomName: getConferenceNameForTitle(state),
379 393
         _showLobby: getIsLobbyVisible(state),
380 394
         _showPrejoin: isPrejoinPageVisible(state) || isPrejoinPageLoading(state)

+ 0
- 188
react/features/notifications/components/AbstractNotificationsContainer.js View File

@@ -1,188 +0,0 @@
1
-// @flow
2
-
3
-import { Component } from 'react';
4
-
5
-import { hideNotification } from '../actions';
6
-import { areThereNotifications } from '../functions';
7
-
8
-export type Props = {
9
-
10
-    /**
11
-     * The notifications to be displayed, with the first index being the
12
-     * notification at the top and the rest shown below it in order.
13
-     */
14
-    _notifications: Array<Object>,
15
-
16
-    /**
17
-     * The length, in milliseconds, to use as a default timeout for all
18
-     * dismissable timeouts that do not have a timeout specified.
19
-     */
20
-    autoDismissTimeout: number,
21
-
22
-    /**
23
-     * Invoked to update the redux store in order to remove notifications.
24
-     */
25
-    dispatch: Function
26
-};
27
-
28
-declare var interfaceConfig: Object;
29
-
30
-/**
31
- * Abstract class for {@code NotificationsContainer} component.
32
- */
33
-export default class AbstractNotificationsContainer<P: Props>
34
-    extends Component<P> {
35
-    /**
36
-     * A timeout id returned by setTimeout.
37
-     */
38
-    _notificationDismissTimeout: ?TimeoutID;
39
-
40
-    /**
41
-     * Initializes a new {@code AbstractNotificationsContainer} instance.
42
-     *
43
-     * @inheritdoc
44
-     */
45
-    constructor(props: P) {
46
-        super(props);
47
-
48
-        /**
49
-         * The timeout set for automatically dismissing a displayed
50
-         * notification. This value is set on the instance and not state to
51
-         * avoid additional re-renders.
52
-         *
53
-         * @type {number|null}
54
-         */
55
-        this._notificationDismissTimeout = null;
56
-
57
-        // Bind event handlers so they are only bound once for every instance.
58
-        this._onDismissed = this._onDismissed.bind(this);
59
-    }
60
-
61
-    /**
62
-     * Sets a timeout for the first notification (if applicable).
63
-     *
64
-     * @inheritdoc
65
-     */
66
-    componentDidMount() {
67
-        // Set the initial dismiss timeout (if any)
68
-        this._manageDismissTimeout();
69
-    }
70
-
71
-    /**
72
-     * Sets a timeout if the currently displayed notification has changed.
73
-     *
74
-     * @inheritdoc
75
-     */
76
-    componentDidUpdate(prevProps: P) {
77
-        this._manageDismissTimeout(prevProps);
78
-    }
79
-
80
-    /**
81
-     * Sets/clears the dismiss timeout for the top notification.
82
-     *
83
-     * @param {P} [prevProps] - The previous properties (if called from
84
-     * {@code componentDidUpdate}).
85
-     * @returns {void}
86
-     * @private
87
-     */
88
-    _manageDismissTimeout(prevProps: ?P) {
89
-        const { _notifications, autoDismissTimeout } = this.props;
90
-
91
-        if (_notifications.length) {
92
-            const notification = _notifications[0];
93
-            const previousNotification
94
-                = prevProps && prevProps._notifications.length
95
-                    ? prevProps._notifications[0]
96
-                    : undefined;
97
-
98
-            if (notification !== previousNotification) {
99
-                this._clearNotificationDismissTimeout();
100
-
101
-                if (notification
102
-                        && (notification.timeout
103
-                            || typeof autoDismissTimeout === 'number')
104
-                        && notification.props.isDismissAllowed !== false) {
105
-                    const {
106
-                        timeout = autoDismissTimeout,
107
-                        uid
108
-                    } = notification;
109
-
110
-                    this._notificationDismissTimeout = setTimeout(() => {
111
-                        // Perform a no-op if a timeout is not specified.
112
-                        this._onDismissed(uid);
113
-                    }, timeout);
114
-                }
115
-            }
116
-        } else if (this._notificationDismissTimeout) {
117
-            // Clear timeout when all notifications are cleared (e.g external
118
-            // call to clear them)
119
-            this._clearNotificationDismissTimeout();
120
-        }
121
-    }
122
-
123
-    /**
124
-     * Clear any dismissal timeout that is still active.
125
-     *
126
-     * @inheritdoc
127
-     * returns {void}
128
-     */
129
-    componentWillUnmount() {
130
-        this._clearNotificationDismissTimeout();
131
-    }
132
-
133
-    _onDismissed: number => void;
134
-
135
-    /**
136
-     * Clears the running notification dismiss timeout, if any.
137
-     *
138
-     * @returns {void}
139
-     */
140
-    _clearNotificationDismissTimeout() {
141
-        this._notificationDismissTimeout
142
-            && clearTimeout(this._notificationDismissTimeout);
143
-
144
-        this._notificationDismissTimeout = null;
145
-    }
146
-
147
-    /**
148
-     * Emits an action to remove the notification from the redux store so it
149
-     * stops displaying.
150
-     *
151
-     * @param {number} uid - The id of the notification to be removed.
152
-     * @private
153
-     * @returns {void}
154
-     */
155
-    _onDismissed(uid) {
156
-        const { _notifications } = this.props;
157
-
158
-        // Clear the timeout only if it's the top notification that's being
159
-        // dismissed (the timeout is set only for the top one).
160
-        if (!_notifications.length || _notifications[0].uid === uid) {
161
-            this._clearNotificationDismissTimeout();
162
-        }
163
-
164
-        this.props.dispatch(hideNotification(uid));
165
-    }
166
-}
167
-
168
-/**
169
- * Maps (parts of) the Redux state to the associated NotificationsContainer's
170
- * props.
171
- *
172
- * @param {Object} state - The Redux state.
173
- * @private
174
- * @returns {{
175
- *     _notifications: Array
176
- * }}
177
- */
178
-export function _abstractMapStateToProps(state: Object) {
179
-    const { notifications } = state['features/notifications'];
180
-    const _visible = areThereNotifications(state);
181
-
182
-    return {
183
-        _notifications: _visible ? notifications : [],
184
-        autoDismissTimeout: typeof interfaceConfig === 'undefined'
185
-            ? undefined // Ignore for the case of mobile
186
-            : interfaceConfig.ENFORCE_NOTIFICATION_AUTO_DISMISS_TIMEOUT
187
-    };
188
-}

+ 162
- 11
react/features/notifications/components/native/NotificationsContainer.js View File

@@ -1,18 +1,27 @@
1 1
 // @flow
2 2
 
3
-import React from 'react';
3
+import React, { Component } from 'react';
4 4
 import { View } from 'react-native';
5 5
 
6 6
 import { connect } from '../../../base/redux';
7
-import AbstractNotificationsContainer, {
8
-    _abstractMapStateToProps,
9
-    type Props as AbstractProps
10
-} from '../AbstractNotificationsContainer';
7
+import { hideNotification } from '../../actions';
8
+import { areThereNotifications } from '../../functions';
11 9
 
12 10
 import Notification from './Notification';
13 11
 import styles from './styles';
14 12
 
15
-type Props = AbstractProps & {
13
+type Props = {
14
+
15
+    /**
16
+     * The notifications to be displayed, with the first index being the
17
+     * notification at the top and the rest shown below it in order.
18
+     */
19
+    _notifications: Array<Object>,
20
+
21
+    /**
22
+     * Invoked to update the redux store in order to remove notifications.
23
+     */
24
+     dispatch: Function,
16 25
 
17 26
     /**
18 27
      * Any custom styling applied to the notifications container.
@@ -27,8 +36,134 @@ type Props = AbstractProps & {
27 36
  *
28 37
  * @extends {Component}
29 38
  */
30
-class NotificationsContainer
31
-    extends AbstractNotificationsContainer<Props> {
39
+class NotificationsContainer extends Component<Props> {
40
+
41
+    /**
42
+     * A timeout id returned by setTimeout.
43
+     */
44
+    _notificationDismissTimeout: ?TimeoutID;
45
+
46
+    /**
47
+     * Initializes a new {@code NotificationsContainer} instance.
48
+     *
49
+     * @inheritdoc
50
+     */
51
+    constructor(props: Props) {
52
+        super(props);
53
+
54
+        /**
55
+         * The timeout set for automatically dismissing a displayed
56
+         * notification. This value is set on the instance and not state to
57
+         * avoid additional re-renders.
58
+         *
59
+         * @type {number|null}
60
+         */
61
+        this._notificationDismissTimeout = null;
62
+
63
+        // Bind event handlers so they are only bound once for every instance.
64
+        this._onDismissed = this._onDismissed.bind(this);
65
+    }
66
+
67
+    /**
68
+     * Sets a timeout for the first notification (if applicable).
69
+     *
70
+     * @inheritdoc
71
+     */
72
+    componentDidMount() {
73
+        // Set the initial dismiss timeout (if any)
74
+        this._manageDismissTimeout();
75
+    }
76
+
77
+    /**
78
+     * Sets a timeout if the currently displayed notification has changed.
79
+     *
80
+     * @inheritdoc
81
+     */
82
+    componentDidUpdate(prevProps: Props) {
83
+        this._manageDismissTimeout(prevProps);
84
+    }
85
+
86
+    /**
87
+     * Sets/clears the dismiss timeout for the top notification.
88
+     *
89
+     * @param {P} [prevProps] - The previous properties (if called from
90
+     * {@code componentDidUpdate}).
91
+     * @returns {void}
92
+     * @private
93
+     */
94
+    _manageDismissTimeout(prevProps: ?Props) {
95
+        const { _notifications } = this.props;
96
+
97
+        if (_notifications.length) {
98
+            const notification = _notifications[0];
99
+            const previousNotification
100
+                 = prevProps && prevProps._notifications.length
101
+                     ? prevProps._notifications[0]
102
+                     : undefined;
103
+
104
+            if (notification !== previousNotification) {
105
+                this._clearNotificationDismissTimeout();
106
+
107
+                if (notification && notification.timeout && notification.props.isDismissAllowed !== false) {
108
+                    const {
109
+                        timeout,
110
+                        uid
111
+                    } = notification;
112
+
113
+                    this._notificationDismissTimeout = setTimeout(() => {
114
+                        // Perform a no-op if a timeout is not specified.
115
+                        this._onDismissed(uid);
116
+                    }, timeout);
117
+                }
118
+            }
119
+        } else if (this._notificationDismissTimeout) {
120
+            // Clear timeout when all notifications are cleared (e.g external
121
+            // call to clear them)
122
+            this._clearNotificationDismissTimeout();
123
+        }
124
+    }
125
+
126
+    /**
127
+     * Clear any dismissal timeout that is still active.
128
+     *
129
+     * @inheritdoc
130
+     */
131
+    componentWillUnmount() {
132
+        this._clearNotificationDismissTimeout();
133
+    }
134
+
135
+    /**
136
+     * Clears the running notification dismiss timeout, if any.
137
+     *
138
+     * @returns {void}
139
+     */
140
+    _clearNotificationDismissTimeout() {
141
+        this._notificationDismissTimeout && clearTimeout(this._notificationDismissTimeout);
142
+
143
+        this._notificationDismissTimeout = null;
144
+    }
145
+
146
+    _onDismissed: number => void;
147
+
148
+    /**
149
+     * Emits an action to remove the notification from the redux store so it
150
+     * stops displaying.
151
+     *
152
+     * @param {number} uid - The id of the notification to be removed.
153
+     * @private
154
+     * @returns {void}
155
+     */
156
+    _onDismissed(uid) {
157
+        const { _notifications } = this.props;
158
+
159
+        // Clear the timeout only if it's the top notification that's being
160
+        // dismissed (the timeout is set only for the top one).
161
+        if (!_notifications.length || _notifications[0].uid === uid) {
162
+            this._clearNotificationDismissTimeout();
163
+        }
164
+
165
+        this.props.dispatch(hideNotification(uid));
166
+    }
32 167
 
33 168
     /**
34 169
      * Implements React's {@link Component#render()}.
@@ -39,8 +174,7 @@ class NotificationsContainer
39 174
         const { _notifications } = this.props;
40 175
 
41 176
         // Currently the native container displays only the topmost notification
42
-        const theNotification
43
-            = _notifications && _notifications.length && _notifications[0];
177
+        const theNotification = _notifications[0];
44 178
 
45 179
         if (!theNotification) {
46 180
             return null;
@@ -64,4 +198,21 @@ class NotificationsContainer
64 198
     _onDismissed: number => void;
65 199
 }
66 200
 
67
-export default connect(_abstractMapStateToProps)(NotificationsContainer);
201
+/**
202
+ * Maps (parts of) the Redux state to the associated NotificationsContainer's
203
+ * props.
204
+ *
205
+ * @param {Object} state - The Redux state.
206
+ * @private
207
+ * @returns {Props}
208
+ */
209
+export function mapStateToProps(state: Object) {
210
+    const { notifications } = state['features/notifications'];
211
+    const _visible = areThereNotifications(state);
212
+
213
+    return {
214
+        _notifications: _visible ? notifications : []
215
+    };
216
+}
217
+
218
+export default connect(mapStateToProps)(NotificationsContainer);

+ 11
- 35
react/features/notifications/components/web/Notification.js View File

@@ -2,12 +2,10 @@
2 2
 
3 3
 import Flag from '@atlaskit/flag';
4 4
 import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info';
5
-import ErrorIcon from '@atlaskit/icon/glyph/error';
6
-import WarningIcon from '@atlaskit/icon/glyph/warning';
7
-import { colors } from '@atlaskit/theme';
8 5
 import React from 'react';
9 6
 
10 7
 import { translate } from '../../../base/i18n';
8
+import { colors } from '../../../base/ui/Tokens';
11 9
 import { NOTIFICATION_TYPE } from '../../constants';
12 10
 import AbstractNotification, {
13 11
     type Props
@@ -21,11 +19,9 @@ declare var interfaceConfig: Object;
21 19
  * @type {{error, info, normal, success, warning}}
22 20
  */
23 21
 const ICON_COLOR = {
24
-    error: colors.R400,
25
-    info: colors.N500,
26
-    normal: colors.N0,
27
-    success: colors.G400,
28
-    warning: colors.Y200
22
+    error: colors.error06,
23
+    normal: colors.primary06,
24
+    warning: colors.warning05
29 25
 };
30 26
 
31 27
 /**
@@ -42,7 +38,6 @@ class Notification extends AbstractNotification<Props> {
42 38
      */
43 39
     render() {
44 40
         const {
45
-            appearance,
46 41
             hideErrorSupportLink,
47 42
             t,
48 43
             title,
@@ -54,7 +49,6 @@ class Notification extends AbstractNotification<Props> {
54 49
         return (
55 50
             <Flag
56 51
                 actions = { this._mapAppearanceToButtons(hideErrorSupportLink) }
57
-                appearance = { appearance }
58 52
                 description = { this._renderDescription() }
59 53
                 icon = { this._mapAppearanceToIcon() }
60 54
                 id = { uid }
@@ -163,31 +157,13 @@ class Notification extends AbstractNotification<Props> {
163 157
         const secIconColor = ICON_COLOR[this.props.appearance];
164 158
         const iconSize = 'medium';
165 159
 
166
-        switch (appearance) {
167
-        case NOTIFICATION_TYPE.ERROR:
168
-            return (
169
-                <ErrorIcon
170
-                    label = { appearance }
171
-                    secondaryColor = { secIconColor }
172
-                    size = { iconSize } />
173
-            );
174
-
175
-        case NOTIFICATION_TYPE.WARNING:
176
-            return (
177
-                <WarningIcon
178
-                    label = { appearance }
179
-                    secondaryColor = { secIconColor }
180
-                    size = { iconSize } />
181
-            );
182
-
183
-        default:
184
-            return (
185
-                <EditorInfoIcon
186
-                    label = { appearance }
187
-                    secondaryColor = { secIconColor }
188
-                    size = { iconSize } />
189
-            );
190
-        }
160
+        return <>
161
+            <div className = { `ribbon ${appearance}` } />
162
+            <EditorInfoIcon
163
+                label = { appearance }
164
+                secondaryColor = { secIconColor }
165
+                size = { iconSize } />
166
+        </>;
191 167
     }
192 168
 }
193 169
 

+ 226
- 27
react/features/notifications/components/web/NotificationsContainer.js View File

@@ -1,38 +1,173 @@
1 1
 // @flow
2 2
 
3
-import { FlagGroup } from '@atlaskit/flag';
4
-import React from 'react';
3
+import { FlagGroupContext } from '@atlaskit/flag/flag-group';
4
+import { AtlasKitThemeProvider } from '@atlaskit/theme';
5
+import { withStyles } from '@material-ui/styles';
6
+import React, { Component } from 'react';
7
+import { CSSTransition, TransitionGroup } from 'react-transition-group';
5 8
 
6 9
 import { translate } from '../../../base/i18n';
7 10
 import { connect } from '../../../base/redux';
8
-import AbstractNotificationsContainer, {
9
-    _abstractMapStateToProps,
10
-    type Props as AbstractProps
11
-} from '../AbstractNotificationsContainer';
11
+import { hideNotification } from '../../actions';
12
+import { areThereNotifications } from '../../functions';
12 13
 
13 14
 import Notification from './Notification';
14 15
 
15
-type Props = AbstractProps & {
16
+declare var interfaceConfig: Object;
17
+
18
+type Props = {
16 19
 
17 20
     /**
18 21
      * Whether we are a SIP gateway or not.
19 22
      */
20
-     _iAmSipGateway: boolean,
23
+    _iAmSipGateway: boolean,
24
+
25
+    /**
26
+     * Whether or not the chat is open.
27
+     */
28
+    _isChatOpen: boolean,
29
+
30
+    /**
31
+     * The notifications to be displayed, with the first index being the
32
+     * notification at the top and the rest shown below it in order.
33
+     */
34
+    _notifications: Array<Object>,
35
+
36
+    /**
37
+     * The length, in milliseconds, to use as a default timeout for all
38
+     * dismissible timeouts that do not have a timeout specified.
39
+     */
40
+    autoDismissTimeout: number,
41
+
42
+    /**
43
+     * JSS classes object.
44
+     */
45
+    classes: Object,
46
+
47
+    /**
48
+     * Invoked to update the redux store in order to remove notifications.
49
+     */
50
+    dispatch: Function,
51
+
52
+    /**
53
+     * Whether or not the notifications are displayed in a portal.
54
+     */
55
+    portal?: boolean,
21 56
 
22
-     /**
57
+    /**
23 58
      * Invoked to obtain translated strings.
24 59
      */
25
-     t: Function
60
+    t: Function
61
+};
62
+
63
+const useStyles = theme => {
64
+    return {
65
+        container: {
66
+            position: 'absolute',
67
+            left: '16px',
68
+            bottom: '90px',
69
+            width: '400px',
70
+            maxWidth: '100%',
71
+            zIndex: 600
72
+        },
73
+
74
+        containerPortal: {
75
+            maxWidth: 'calc(100% - 32px)'
76
+        },
77
+
78
+        containerChatOpen: {
79
+            left: '331px'
80
+        },
81
+
82
+        transitionGroup: {
83
+            '& > *': {
84
+                marginBottom: '20px',
85
+                borderRadius: '6px!important', // !important used to overwrite atlaskit style
86
+                position: 'relative'
87
+            },
88
+
89
+            '& div > span > svg > path': {
90
+                fill: 'inherit'
91
+            },
92
+
93
+            '& div > span, & div > p': {
94
+                color: theme.palette.field01
95
+            },
96
+
97
+            '& .ribbon': {
98
+                width: '4px',
99
+                height: 'calc(100% - 16px)',
100
+                position: 'absolute',
101
+                left: 0,
102
+                top: '8px',
103
+                borderRadius: '4px',
104
+
105
+                '&.normal': {
106
+                    backgroundColor: theme.palette.link01Active
107
+                },
108
+
109
+                '&.error': {
110
+                    backgroundColor: theme.palette.iconError
111
+                },
112
+
113
+                '&.warning': {
114
+                    backgroundColor: theme.palette.warning01
115
+                }
116
+            }
117
+        }
118
+    };
26 119
 };
27 120
 
28 121
 /**
29 122
  * Implements a React {@link Component} which displays notifications and handles
30
- * automatic dismissmal after a notification is shown for a defined timeout
123
+ * automatic dismissal after a notification is shown for a defined timeout
31 124
  * period.
32 125
  *
33 126
  * @extends {Component}
34 127
  */
35
-class NotificationsContainer extends AbstractNotificationsContainer<Props> {
128
+class NotificationsContainer extends Component<Props> {
129
+    _api: Object;
130
+    _timeouts: Map<string, TimeoutID>;
131
+
132
+    /**
133
+     * Initializes a new {@code NotificationsContainer} instance.
134
+     *
135
+     * @inheritdoc
136
+     */
137
+    constructor(props: Props) {
138
+        super(props);
139
+
140
+        this._timeouts = new Map();
141
+
142
+        // Bind event handlers so they are only bound once for every instance.
143
+        this._onDismissed = this._onDismissed.bind(this);
144
+
145
+        // HACK ALERT! We are rendering AtlasKit Flag elements outside of a FlagGroup container.
146
+        // In order to hook-up the dismiss action we'll a fake context provider,
147
+        // just like FlagGroup does.
148
+        this._api = {
149
+            onDismissed: this._onDismissed,
150
+            dismissAllowed: () => true
151
+        };
152
+    }
153
+
154
+    /**
155
+     * Sets a timeout for each notification, where applicable.
156
+     *
157
+     * @inheritdoc
158
+     */
159
+    componentDidMount() {
160
+        this._updateTimeouts();
161
+    }
162
+
163
+    /**
164
+     * Sets a timeout for each notification, where applicable.
165
+     *
166
+     * @inheritdoc
167
+     */
168
+    componentDidUpdate() {
169
+        this._updateTimeouts();
170
+    }
36 171
 
37 172
     /**
38 173
      * Implements React's {@link Component#render()}.
@@ -46,17 +181,46 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
46 181
         }
47 182
 
48 183
         return (
49
-            <FlagGroup
50
-                id = 'notifications-container'
51
-                label = { this.props.t('notify.groupTitle') }
52
-                onDismissed = { this._onDismissed }>
53
-                { this._renderFlags() }
54
-            </FlagGroup>
184
+            <AtlasKitThemeProvider mode = 'light'>
185
+                <FlagGroupContext.Provider value = { this._api }>
186
+                    <div
187
+                        className = { `${this.props.classes.container} ${this.props.portal
188
+                            ? this.props.classes.containerPortal
189
+                            : this.props._isChatOpen
190
+                                ? this.props.classes.containerChatOpen
191
+                                : ''}`
192
+                        }
193
+                        id = 'notifications-container'>
194
+                        <TransitionGroup className = { this.props.classes.transitionGroup }>
195
+                            {this._renderFlags()}
196
+                        </TransitionGroup>
197
+                    </div>
198
+                </FlagGroupContext.Provider>
199
+            </AtlasKitThemeProvider>
55 200
         );
56 201
     }
57 202
 
58 203
     _onDismissed: number => void;
59 204
 
205
+    /**
206
+     * Emits an action to remove the notification from the redux store so it
207
+     * stops displaying.
208
+     *
209
+     * @param {number} uid - The id of the notification to be removed.
210
+     * @private
211
+     * @returns {void}
212
+     */
213
+    _onDismissed(uid) {
214
+        const timeout = this._timeouts.get(uid);
215
+
216
+        if (timeout) {
217
+            clearTimeout(timeout);
218
+            this._timeouts.delete(uid);
219
+        }
220
+
221
+        this.props.dispatch(hideNotification(uid));
222
+    }
223
+
60 224
     /**
61 225
      * Renders notifications to display as ReactElements. An empty array will
62 226
      * be returned if notifications are disabled.
@@ -74,15 +238,46 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
74 238
             // either id or key to set a key on notifications, but accessing
75 239
             // props.key will cause React to print an error.
76 240
             return (
77
-                <Notification
78
-                    { ...props }
79
-                    id = { uid }
241
+                <CSSTransition
242
+                    appear = { true }
243
+                    classNames = 'notification'
244
+                    in = { true }
80 245
                     key = { uid }
81
-                    onDismissed = { this._onDismissed }
82
-                    uid = { uid } />
246
+                    timeout = { 200 }>
247
+                    <Notification
248
+                        { ...props }
249
+                        id = { uid }
250
+                        onDismissed = { this._onDismissed }
251
+                        uid = { uid } />
252
+                </CSSTransition>
83 253
             );
84 254
         });
85 255
     }
256
+
257
+    /**
258
+     * Updates the timeouts for every notification.
259
+     *
260
+     * @returns {void}
261
+     */
262
+    _updateTimeouts() {
263
+        const { _notifications, autoDismissTimeout } = this.props;
264
+
265
+        for (const notification of _notifications) {
266
+            if ((notification.timeout || typeof autoDismissTimeout === 'number')
267
+                    && notification.props.isDismissAllowed !== false
268
+                    && !this._timeouts.has(notification.uid)) {
269
+                const {
270
+                    timeout = autoDismissTimeout,
271
+                    uid
272
+                } = notification;
273
+                const timerID = setTimeout(() => {
274
+                    this._onDismissed(uid);
275
+                }, timeout);
276
+
277
+                this._timeouts.set(uid, timerID);
278
+            }
279
+        }
280
+    }
86 281
 }
87 282
 
88 283
 /**
@@ -93,13 +288,17 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
93 288
  * @returns {Props}
94 289
  */
95 290
 function _mapStateToProps(state) {
291
+    const { notifications } = state['features/notifications'];
96 292
     const { iAmSipGateway } = state['features/base/config'];
293
+    const { isOpen: isChatOpen } = state['features/chat'];
294
+    const _visible = areThereNotifications(state);
97 295
 
98 296
     return {
99
-        ..._abstractMapStateToProps(state),
100
-        _iAmSipGateway: Boolean(iAmSipGateway)
297
+        _iAmSipGateway: Boolean(iAmSipGateway),
298
+        _isChatOpen: isChatOpen,
299
+        _notifications: _visible ? notifications : [],
300
+        autoDismissTimeout: interfaceConfig.ENFORCE_NOTIFICATION_AUTO_DISMISS_TIMEOUT
101 301
     };
102 302
 }
103 303
 
104
-
105
-export default translate(connect(_mapStateToProps)(NotificationsContainer));
304
+export default translate(connect(_mapStateToProps)(withStyles(useStyles)(NotificationsContainer)));

+ 3
- 3
react/features/participants-pane/components/web/LobbyParticipants.js View File

@@ -10,7 +10,7 @@ import { Icon, IconCheck, IconClose } from '../../../base/icons';
10 10
 import { withPixelLineHeight } from '../../../base/styles/functions.web';
11 11
 import { admitMultiple } from '../../../lobby/actions.web';
12 12
 import { getLobbyEnabled, getKnockingParticipants } from '../../../lobby/functions';
13
-import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
13
+import { Drawer, JitsiPortal } from '../../../toolbox/components/web';
14 14
 import { showOverflowDrawer } from '../../../toolbox/functions';
15 15
 import { useLobbyActions, useParticipantDrawer } from '../../hooks';
16 16
 
@@ -96,7 +96,7 @@ export default function LobbyParticipants() {
96 96
             openDrawerForParticipant = { openDrawerForParticipant }
97 97
             overflowDrawer = { overflowDrawer }
98 98
             participants = { participants } />
99
-        <DrawerPortal>
99
+        <JitsiPortal>
100 100
             <Drawer
101 101
                 isOpen = { Boolean(drawerParticipant && overflowDrawer) }
102 102
                 onClose = { closeDrawer }>
@@ -128,7 +128,7 @@ export default function LobbyParticipants() {
128 128
                     </li>
129 129
                 </ul>
130 130
             </Drawer>
131
-        </DrawerPortal>
131
+        </JitsiPortal>
132 132
     </>
133 133
     );
134 134
 }

+ 3
- 3
react/features/participants-pane/components/web/MeetingParticipantContextMenu.js View File

@@ -30,7 +30,7 @@ import { withPixelLineHeight } from '../../../base/styles/functions.web';
30 30
 import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
31 31
 import { openChatById } from '../../../chat/actions';
32 32
 import { setVolume } from '../../../filmstrip/actions.web';
33
-import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
33
+import { Drawer, JitsiPortal } from '../../../toolbox/components/web';
34 34
 import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
35 35
 import { VolumeSlider } from '../../../video-menu/components/web';
36 36
 import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
@@ -533,7 +533,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
533 533
                       { actions }
534 534
                   </ContextMenu>}
535 535
 
536
-                <DrawerPortal>
536
+                <JitsiPortal>
537 537
                     <Drawer
538 538
                         isOpen = { drawerParticipant && overflowDrawer }
539 539
                         onClose = { closeDrawer }>
@@ -549,7 +549,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
549 549
                             { actions }
550 550
                         </div>
551 551
                     </Drawer>
552
-                </DrawerPortal>
552
+                </JitsiPortal>
553 553
             </>
554 554
         );
555 555
     }

+ 3
- 3
react/features/participants-pane/components/web/ParticipantsPane.js View File

@@ -7,7 +7,7 @@ import { openDialog } from '../../../base/dialog';
7 7
 import { translate } from '../../../base/i18n';
8 8
 import { isLocalParticipantModerator } from '../../../base/participants';
9 9
 import { connect } from '../../../base/redux';
10
-import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
10
+import { Drawer, JitsiPortal } from '../../../toolbox/components/web';
11 11
 import { showOverflowDrawer } from '../../../toolbox/functions';
12 12
 import { MuteEveryoneDialog } from '../../../video-menu/components/';
13 13
 import { close } from '../../actions';
@@ -166,13 +166,13 @@ class ParticipantsPane extends Component<Props, State> {
166 166
                             </Footer>
167 167
                         )}
168 168
                     </div>
169
-                    <DrawerPortal>
169
+                    <JitsiPortal>
170 170
                         <Drawer
171 171
                             isOpen = { contextOpen && _overflowDrawer }
172 172
                             onClose = { this._onDrawerClose }>
173 173
                             <FooterContextMenu inDrawer = { true } />
174 174
                         </Drawer>
175
-                    </DrawerPortal>
175
+                    </JitsiPortal>
176 176
                 </div>
177 177
             </ThemeProvider>
178 178
         );

react/features/toolbox/components/web/DrawerPortal.js → react/features/toolbox/components/web/JitsiPortal.js View File

@@ -8,7 +8,12 @@ type Props = {
8 8
     /**
9 9
      * The component(s) to be displayed within the drawer portal.
10 10
      */
11
-    children: React$Node
11
+    children: React$Node,
12
+
13
+    /**
14
+     * Class name used to add custom styles to the portal.
15
+     */
16
+    className?: string
12 17
 };
13 18
 
14 19
 /**
@@ -17,12 +22,12 @@ type Props = {
17 22
  *
18 23
  * @returns {ReactElement}
19 24
  */
20
-function DrawerPortal({ children }: Props) {
25
+function JitsiPortal({ children, className }: Props) {
21 26
     return (
22
-        <DialogPortal className = 'drawer-portal'>
27
+        <DialogPortal className = { `drawer-portal ${className ?? ''}` }>
23 28
             { children }
24 29
         </DialogPortal>
25 30
     );
26 31
 }
27 32
 
28
-export default DrawerPortal;
33
+export default JitsiPortal;

+ 3
- 3
react/features/toolbox/components/web/OverflowMenuButton.js View File

@@ -12,7 +12,7 @@ import { type ReactionEmojiProps } from '../../../reactions/constants';
12 12
 import { getReactionsQueue } from '../../../reactions/functions.any';
13 13
 
14 14
 import Drawer from './Drawer';
15
-import DrawerPortal from './DrawerPortal';
15
+import JitsiPortal from './JitsiPortal';
16 16
 import ToolbarButton from './ToolbarButton';
17 17
 
18 18
 /**
@@ -114,7 +114,7 @@ class OverflowMenuButton extends Component<Props> {
114 114
                     overflowDrawer ? (
115 115
                         <>
116 116
                             {this._renderToolbarButton()}
117
-                            <DrawerPortal>
117
+                            <JitsiPortal>
118 118
                                 <Drawer
119 119
                                     isOpen = { isOpen }
120 120
                                     onClose = { this._onCloseDialog }>
@@ -128,7 +128,7 @@ class OverflowMenuButton extends Component<Props> {
128 128
                                         reaction = { reaction }
129 129
                                         uid = { uid } />))}
130 130
                                 </div>}
131
-                            </DrawerPortal>
131
+                            </JitsiPortal>
132 132
                         </>
133 133
                     ) : (
134 134
                         <InlineDialog

+ 1
- 1
react/features/toolbox/components/web/index.js View File

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

Loading…
Cancel
Save