Browse Source

feat(callee-info): Redesign.

master
hristoterezov 6 years ago
parent
commit
769e782c6f
34 changed files with 533 additions and 602 deletions
  1. 12
    0
      conference.js
  2. 15
    11
      css/ringing/_ringing.scss
  3. 5
    5
      lang/main.json
  4. 3
    1
      modules/API/API.js
  5. 1
    4
      modules/API/external/external_api.js
  6. 4
    1
      modules/UI/UI.js
  7. 6
    1
      modules/UI/videolayout/LargeVideoManager.js
  8. 6
    1
      modules/UI/videolayout/RemoteVideo.js
  9. 9
    0
      react/features/base/conference/actions.js
  10. 0
    10
      react/features/base/jwt/actionTypes.js
  11. 1
    23
      react/features/base/jwt/actions.js
  12. 0
    347
      react/features/base/jwt/components/CalleeInfo.js
  13. 0
    1
      react/features/base/jwt/index.js
  14. 2
    86
      react/features/base/jwt/middleware.js
  15. 2
    14
      react/features/base/jwt/reducer.js
  16. 2
    0
      react/features/base/participants/reducer.js
  17. 1
    1
      react/features/conference/components/Conference.native.js
  18. 1
    1
      react/features/conference/components/Conference.web.js
  19. 4
    0
      react/features/filmstrip/functions.web.js
  20. 0
    41
      react/features/filmstrip/middleware.js
  21. 38
    6
      react/features/invite/actionTypes.js
  22. 110
    24
      react/features/invite/actions.js
  23. 161
    0
      react/features/invite/components/callee-info/CalleeInfo.js
  24. 1
    1
      react/features/invite/components/callee-info/CalleeInfoContainer.js
  25. 0
    1
      react/features/invite/components/callee-info/index.js
  26. 1
    1
      react/features/invite/components/callee-info/styles.native.js
  27. 0
    0
      react/features/invite/components/callee-info/styles.web.js
  28. 1
    0
      react/features/invite/components/index.js
  29. 86
    12
      react/features/invite/middleware.any.js
  30. 33
    1
      react/features/invite/reducer.js
  31. 3
    1
      react/features/notifications/components/AbstractNotificationsContainer.js
  32. 23
    6
      react/features/presence-status/components/PresenceLabel.js
  33. 1
    0
      react/features/presence-status/constants.js
  34. 1
    1
      react/features/toolbox/actions.web.js

+ 12
- 0
conference.js View File

@@ -1662,6 +1662,7 @@ export default {
1662 1662
             const displayName = user.getDisplayName();
1663 1663
 
1664 1664
             APP.store.dispatch(participantJoined({
1665
+                botType: user.getBotType(),
1665 1666
                 conference: room,
1666 1667
                 id,
1667 1668
                 name: displayName,
@@ -1862,6 +1863,17 @@ export default {
1862 1863
                 APP.UI.changeDisplayName(id, formattedDisplayName);
1863 1864
             }
1864 1865
         );
1866
+        room.on(
1867
+            JitsiConferenceEvents.BOT_TYPE_CHANGED,
1868
+            (id, botType) => {
1869
+
1870
+                APP.store.dispatch(participantUpdated({
1871
+                    conference: room,
1872
+                    id,
1873
+                    botType
1874
+                }));
1875
+            }
1876
+        );
1865 1877
 
1866 1878
         room.on(
1867 1879
             JitsiConferenceEvents.LOCK_STATE_CHANGED,

+ 15
- 11
css/ringing/_ringing.scss View File

@@ -6,12 +6,10 @@
6 6
     height: 100%;
7 7
     position: fixed;
8 8
     z-index: $ringingZ;
9
-    background: linear-gradient(transparent, #000);
10
-    opacity: 0.8;
9
+    @include transparentBg(#283447, 0.95);
11 10
 
12 11
     &.solidBG {
13 12
         background: $defaultBackground;
14
-        opacity: 1;
15 13
     }
16 14
 
17 15
     &__content {
@@ -22,20 +20,26 @@
22 20
         top: 50%;
23 21
         margin-left: -200px;
24 22
         margin-top: -125px;
25
-        font-weight: 400;
26
-        font-size: 14px;
27 23
         text-align: center;
24
+        font-weight: normal;
25
+        color: #FFFFFF;
28 26
     }
29 27
 
30 28
     &__avatar {
31
-        width: 100px;
32
-        height: 100px;
29
+        width: 128px;
30
+        height: 128px;
33 31
         border-radius: 50%;
32
+        border: 2px solid #1B2638;
33
+    }
34
+
35
+    &__status{
36
+        margin-top: 15px;
37
+        font-size: 14px;
38
+        line-height: 20px;
34 39
     }
35 40
 
36
-    &__caller-info {
37
-        .mention {
38
-            color: #333;
39
-        }
41
+    &__name {
42
+        font-size: 24px;
43
+        line-height: 32px;
40 44
     }
41 45
 }

+ 5
- 5
lang/main.json View File

@@ -632,12 +632,12 @@
632 632
     },
633 633
     "presenceStatus": {
634 634
         "invited": "Invited",
635
-        "ringing": "Ringing",
636
-        "calling": "Calling",
637
-        "initializingCall": "Initializing Call",
635
+        "ringing": "Ringing...",
636
+        "calling": "Calling...",
637
+        "initializingCall": "Initializing Call...",
638 638
         "connected": "Connected",
639
-        "connecting": "Connecting",
640
-        "connecting2": "Connecting*",
639
+        "connecting": "Connecting...",
640
+        "connecting2": "Connecting*...",
641 641
         "disconnected": "Disconnected",
642 642
         "busy": "Busy",
643 643
         "rejected": "Rejected",

+ 3
- 1
modules/API/API.js View File

@@ -113,8 +113,10 @@ function initCommands() {
113 113
 
114 114
         switch (name) {
115 115
         case 'invite':
116
+            // The store should be already available because API.init is called
117
+            // on appWillMount action.
116 118
             APP.store.dispatch(
117
-                invite(request.invitees))
119
+                invite(request.invitees, true))
118 120
                 .then(failedInvitees => {
119 121
                     let error;
120 122
                     let result;

+ 1
- 4
modules/API/external/external_api.js View File

@@ -238,7 +238,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
238 238
                 }
239 239
             })
240 240
         });
241
-        this._invitees = invitees;
241
+        this.invite(invitees);
242 242
         this._isLargeVideoVisible = true;
243 243
         this._numberOfParticipants = 0;
244 244
         this._participants = {};
@@ -369,9 +369,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
369 369
 
370 370
             switch (name) {
371 371
             case 'video-conference-joined':
372
-                if (this._invitees) {
373
-                    this.invite(this._invitees);
374
-                }
375 372
                 this._myUserID = userID;
376 373
                 this._participants[userID] = {
377 374
                     avatarURL: data.avatarURL

+ 4
- 1
modules/UI/UI.js View File

@@ -492,7 +492,10 @@ UI.updateUserRole = user => {
492 492
  * @param {string} status - The new status.
493 493
  */
494 494
 UI.updateUserStatus = (user, status) => {
495
-    if (!status) {
495
+    const reduxState = APP.store.getState() || {};
496
+    const { calleeInfoVisible } = reduxState['features/invite'] || {};
497
+
498
+    if (!status || calleeInfoVisible) {
496 499
         return;
497 500
     }
498 501
 

+ 6
- 1
modules/UI/videolayout/LargeVideoManager.js View File

@@ -428,7 +428,12 @@ export default class LargeVideoManager {
428 428
             ReactDOM.render(
429 429
                 <Provider store = { APP.store }>
430 430
                     <I18nextProvider i18n = { i18next }>
431
-                        <PresenceLabel participantID = { id } />
431
+                        <PresenceLabel
432
+                            noContentStyles = { {
433
+                                className: 'presence-label no-presence'
434
+                            } }
435
+                            participantID = { id }
436
+                            styles = { { className: 'presence-label' } } />
432 437
                     </I18nextProvider>
433 438
                 </Provider>,
434 439
                 presenceLabelContainer.get(0));

+ 6
- 1
modules/UI/videolayout/RemoteVideo.js View File

@@ -573,7 +573,12 @@ RemoteVideo.prototype.addPresenceLabel = function() {
573 573
         ReactDOM.render(
574 574
             <Provider store = { APP.store }>
575 575
                 <I18nextProvider i18n = { i18next }>
576
-                    <PresenceLabel participantID = { this.id } />
576
+                    <PresenceLabel
577
+                        noContentStyles = { {
578
+                            className: 'presence-label no-presence'
579
+                        } }
580
+                        participantID = { this.id }
581
+                        styles = { { className: 'presence-label' } } />
577 582
                 </I18nextProvider>
578 583
             </Provider>,
579 584
             presenceLabelContainer);

+ 9
- 0
react/features/base/conference/actions.js View File

@@ -144,6 +144,7 @@ function _addConferenceListeners(conference, dispatch) {
144 144
     conference.on(
145 145
         JitsiConferenceEvents.USER_JOINED,
146 146
         (id, user) => !user.isHidden() && dispatch(participantJoined({
147
+            botType: user.getBotType(),
147 148
             conference,
148 149
             id,
149 150
             name: user.getDisplayName(),
@@ -161,6 +162,14 @@ function _addConferenceListeners(conference, dispatch) {
161 162
         JitsiConferenceEvents.USER_STATUS_CHANGED,
162 163
         (...args) => dispatch(participantPresenceChanged(...args)));
163 164
 
165
+    conference.on(
166
+        JitsiConferenceEvents.BOT_TYPE_CHANGED,
167
+        (id, botType) => dispatch(participantUpdated({
168
+            conference,
169
+            id,
170
+            botType
171
+        })));
172
+
164 173
     conference.addCommandListener(
165 174
         AVATAR_ID_COMMAND,
166 175
         (data, id) => dispatch(participantUpdated({

+ 0
- 10
react/features/base/jwt/actionTypes.js View File

@@ -1,13 +1,3 @@
1
-/**
2
- * The type of redux action which sets the visibility of {@code CalleeInfo}.
3
- *
4
- * {
5
- *     type: SET_CALLEE_INFO_VISIBLE,
6
- *     calleeInfoVisible: boolean
7
- * }
8
- */
9
-export const SET_CALLEE_INFO_VISIBLE = Symbol('SET_CALLEE_INFO_VISIBLE');
10
-
11 1
 /**
12 2
  * The type of redux action which stores a specific JSON Web Token (JWT) into
13 3
  * the redux store.

+ 1
- 23
react/features/base/jwt/actions.js View File

@@ -1,28 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { SET_CALLEE_INFO_VISIBLE, SET_JWT } from './actionTypes';
4
-
5
-/**
6
- * Sets the visibility of {@code CalleeInfo}.
7
- *
8
- * @param {boolean|undefined} [calleeInfoVisible] - If {@code CalleeInfo} is
9
- * to be displayed/visible, then {@code true}; otherwise, {@code false} or
10
- * {@code undefined}.
11
- * @returns {{
12
- *     type: SET_CALLEE_INFO_VISIBLE,
13
- *     calleeInfoVisible: (boolean|undefined)
14
- * }}
15
- */
16
-export function setCalleeInfoVisible(calleeInfoVisible: ?boolean) {
17
-    return (dispatch: Dispatch<*>, getState: Function) => {
18
-        getState()['features/base/jwt']
19
-            .calleeInfoVisible === calleeInfoVisible
20
-            || dispatch({
21
-                type: SET_CALLEE_INFO_VISIBLE,
22
-                calleeInfoVisible
23
-            });
24
-    };
25
-}
3
+import { SET_JWT } from './actionTypes';
26 4
 
27 5
 /**
28 6
  * Stores a specific JSON Web Token (JWT) into the redux store.

+ 0
- 347
react/features/base/jwt/components/CalleeInfo.js View File

@@ -1,347 +0,0 @@
1
-// @flow
2
-
3
-import React, { Component } from 'react';
4
-import { connect } from 'react-redux';
5
-
6
-import { Audio } from '../../media';
7
-import { Avatar } from '../../participants';
8
-import { Container, Text } from '../../react';
9
-import UIEvents from '../../../../../service/UI/UIEvents';
10
-
11
-import styles from './styles';
12
-
13
-declare var $: Object;
14
-declare var APP: Object;
15
-declare var interfaceConfig: Object;
16
-
17
-/**
18
- * The type of the React {@code Component} props of {@link CalleeInfo}.
19
- */
20
-type Props = {
21
-
22
-    /**
23
-     * The callee's information such as avatar and display name.
24
-     */
25
-    _callee: Object
26
-};
27
-
28
-/**
29
- * The type of the React {@code Component} state of {@link CalleeInfo}.
30
- */
31
-type State = {
32
-
33
-    /**
34
-     * The CSS class (name), if any, to add to this {@code CalleeInfo}.
35
-     *
36
-     * @type {string}
37
-     */
38
-    className: ?string,
39
-
40
-    /**
41
-     * The indicator which determines whether this {@code CalleeInfo}
42
-     * should play/render audio to indicate the ringing phase of the
43
-     * call establishment between the local participant and the
44
-     * associated remote callee.
45
-     *
46
-     * @type {boolean}
47
-     */
48
-    renderAudio: boolean,
49
-
50
-    /**
51
-     * The indicator which determines whether this {@code CalleeInfo}
52
-     * is depicting the ringing phase of the call establishment between
53
-     * the local participant and the associated remote callee or the
54
-     * phase afterwards when the callee has not answered the call for a
55
-     * period of time and, consequently, is considered unavailable.
56
-     *
57
-     * @type {boolean}
58
-     */
59
-    ringing: boolean
60
-};
61
-
62
-/**
63
- * Implements a React {@link Component} which depicts the establishment of a
64
- * call with a specific remote callee.
65
- *
66
- * @extends Component
67
- */
68
-class CalleeInfo extends Component<Props, State> {
69
-    /**
70
-     * The (reference to the) {@link Audio} which plays/renders the audio
71
-     * depicting the ringing phase of the call establishment represented by this
72
-     * {@code CalleeInfo}.
73
-     */
74
-    _audio: ?Audio;
75
-
76
-    _onLargeVideoAvatarVisible: Function;
77
-
78
-    _playAudioInterval: ?IntervalID;
79
-
80
-    _ringingTimeout: ?TimeoutID;
81
-
82
-    _setAudio: Function;
83
-
84
-    /**
85
-     * Initializes a new {@code CalleeInfo} instance.
86
-     *
87
-     * @param {Object} props - The read-only React {@link Component} props with
88
-     * which the new instance is to be initialized.
89
-     */
90
-    constructor(props) {
91
-        super(props);
92
-
93
-        this.state = {
94
-            className: undefined,
95
-            renderAudio:
96
-                typeof interfaceConfig !== 'object'
97
-                    || !interfaceConfig.DISABLE_RINGING,
98
-            ringing: true
99
-        };
100
-
101
-        this._onLargeVideoAvatarVisible
102
-            = this._onLargeVideoAvatarVisible.bind(this);
103
-        this._setAudio = this._setAudio.bind(this);
104
-
105
-        if (typeof APP === 'object') {
106
-            APP.UI.addListener(
107
-                UIEvents.LARGE_VIDEO_AVATAR_VISIBLE,
108
-                this._onLargeVideoAvatarVisible);
109
-        }
110
-    }
111
-
112
-    /**
113
-     * Sets up timeouts such as the timeout to end the ringing phase of the call
114
-     * establishment depicted by this {@code CalleeInfo}.
115
-     *
116
-     * @inheritdoc
117
-     */
118
-    componentDidMount() {
119
-        // Set up the timeout to end the ringing phase of the call establishment
120
-        // depicted by this CalleeInfo.
121
-        if (this.state.ringing && !this._ringingTimeout) {
122
-            this._ringingTimeout
123
-                = setTimeout(
124
-                    () => {
125
-                        this._pauseAudio();
126
-
127
-                        this._ringingTimeout = undefined;
128
-                        this.setState({
129
-                            ringing: false
130
-                        });
131
-                    },
132
-                    30000);
133
-        }
134
-
135
-        this._playAudio();
136
-    }
137
-
138
-    /**
139
-     * Cleans up before this {@code Calleverlay} is unmounted and destroyed.
140
-     *
141
-     * @inheritdoc
142
-     */
143
-    componentWillUnmount() {
144
-        this._pauseAudio();
145
-
146
-        if (this._ringingTimeout) {
147
-            clearTimeout(this._ringingTimeout);
148
-            this._ringingTimeout = undefined;
149
-        }
150
-
151
-        if (typeof APP === 'object') {
152
-            APP.UI.removeListener(
153
-                UIEvents.LARGE_VIDEO_AVATAR_VISIBLE,
154
-                this._onLargeVideoAvatarVisible);
155
-        }
156
-    }
157
-
158
-    /**
159
-     * Implements React's {@link Component#render()}.
160
-     *
161
-     * @inheritdoc
162
-     * @returns {ReactElement}
163
-     */
164
-    render() {
165
-        const { className, ringing } = this.state;
166
-        const { avatarUrl, avatar, name } = this.props._callee;
167
-
168
-        return (
169
-            <Container
170
-                { ...this._style('ringing', className) }
171
-                id = 'ringOverlay'>
172
-                <Container
173
-                    { ...this._style('ringing__content') }>
174
-                    <Text { ...this._style('ringing__text') }>
175
-                        { ringing ? 'Calling...' : '' }
176
-                    </Text>
177
-                    <Avatar
178
-                        { ...this._style('ringing__avatar') }
179
-                        uri = { avatarUrl || avatar } />
180
-                    <Container
181
-                        { ...this._style('ringing__caller-info') }>
182
-                        <Text
183
-                            { ...this._style('ringing__text') }>
184
-                            { name }
185
-                            { ringing ? '' : ' isn\'t available' }
186
-                        </Text>
187
-                    </Container>
188
-                </Container>
189
-                { this._renderAudio() }
190
-            </Container>
191
-        );
192
-    }
193
-
194
-    /**
195
-     * Notifies this {@code CalleeInfo} that the visibility of the
196
-     * participant's avatar in the large video has changed.
197
-     *
198
-     * @param {boolean} largeVideoAvatarVisible - If the avatar in the large
199
-     * video (i.e. of the participant on the stage) is visible, then
200
-     * {@code true}; otherwise, {@code false}.
201
-     * @private
202
-     * @returns {void}
203
-     */
204
-    _onLargeVideoAvatarVisible(largeVideoAvatarVisible: boolean) {
205
-        this.setState({
206
-            className: largeVideoAvatarVisible ? 'solidBG' : undefined
207
-        });
208
-    }
209
-
210
-    /**
211
-     * Stops the playback of the audio which represents the ringing phase of the
212
-     * call establishment depicted by this {@code CalleeInfo}.
213
-     *
214
-     * @private
215
-     * @returns {void}
216
-     */
217
-    _pauseAudio() {
218
-        const audio = this._audio;
219
-
220
-        if (audio) {
221
-            audio.pause();
222
-        }
223
-        if (this._playAudioInterval) {
224
-            clearInterval(this._playAudioInterval);
225
-            this._playAudioInterval = undefined;
226
-        }
227
-    }
228
-
229
-    /**
230
-     * Starts the playback of the audio which represents the ringing phase of
231
-     * the call establishment depicted by this {@code CalleeInfo}.
232
-     *
233
-     * @private
234
-     * @returns {void}
235
-     */
236
-    _playAudio() {
237
-        if (this._audio) {
238
-            this._audio.play();
239
-            if (!this._playAudioInterval) {
240
-                this._playAudioInterval
241
-                    = setInterval(() => this._playAudio(), 5000);
242
-            }
243
-        }
244
-    }
245
-
246
-    /**
247
-     * Renders an audio element to represent the ringing phase of the call
248
-     * establishment represented by this {@code CalleeInfo}.
249
-     *
250
-     * @private
251
-     * @returns {ReactElement}
252
-     */
253
-    _renderAudio() {
254
-        if (this.state.renderAudio && this.state.ringing) {
255
-            return (
256
-                <Audio
257
-                    setRef = { this._setAudio }
258
-                    src = './sounds/ring.ogg' />
259
-            );
260
-        }
261
-
262
-        return null;
263
-    }
264
-
265
-    /**
266
-     * Sets the (reference to the) {@link Audio} which renders the ringing phase
267
-     * of the call establishment represented by this {@code CalleeInfo}.
268
-     *
269
-     * @param {Audio} audio - The (reference to the) {@code Audio} which
270
-     * plays/renders the audio depicting the ringing phase of the call
271
-     * establishment represented by this {@code CalleeInfo}.
272
-     * @private
273
-     * @returns {void}
274
-     */
275
-    _setAudio(audio) {
276
-        this._audio = audio;
277
-    }
278
-
279
-    /**
280
-     * Attempts to convert specified CSS class names into React
281
-     * {@link Component} props {@code style} or {@code className}.
282
-     *
283
-     * @param {Array<string>} classNames - The CSS class names to convert
284
-     * into React {@code Component} props {@code style} or {@code className}.
285
-     * @returns {{
286
-     *     className: string,
287
-     *     style: Object
288
-     * }}
289
-     */
290
-    _style(...classNames: Array<?string>) {
291
-        let className = '';
292
-        let style;
293
-
294
-        for (const aClassName of classNames) {
295
-            if (aClassName) {
296
-                // Attemp to convert aClassName into style.
297
-                if (styles && aClassName in styles) {
298
-                    // React Native will accept an Array as the value of the
299
-                    // style prop. However, I do not know about React.
300
-                    style = {
301
-                        ...style,
302
-                        ...styles[aClassName]
303
-                    };
304
-                } else {
305
-                    // Otherwise, leave it as className.
306
-                    className += aClassName;
307
-                }
308
-            }
309
-        }
310
-
311
-        // Choose which of the className and/or style props has a value and,
312
-        // consequently, must be returned.
313
-        const props = {};
314
-
315
-        if (className) {
316
-            props.className = className;
317
-        }
318
-        if (style) {
319
-            props.style = style;
320
-        }
321
-
322
-        return props;
323
-    }
324
-}
325
-
326
-/**
327
- * Maps (parts of) the redux state to {@code CalleeInfo}'s props.
328
- *
329
- * @param {Object} state - The redux state.
330
- * @private
331
- * @returns {{
332
- *     _callee: Object
333
- * }}
334
- */
335
-function _mapStateToProps(state) {
336
-    return {
337
-        /**
338
-         * The callee's information such as avatar and display name.
339
-         *
340
-         * @private
341
-         * @type {Object}
342
-         */
343
-        _callee: state['features/base/jwt'].callee
344
-    };
345
-}
346
-
347
-export default connect(_mapStateToProps)(CalleeInfo);

+ 0
- 1
react/features/base/jwt/index.js View File

@@ -1,6 +1,5 @@
1 1
 export * from './actions';
2 2
 export * from './actionTypes';
3
-export * from './components';
4 3
 export * from './functions';
5 4
 
6 5
 import './middleware';

+ 2
- 86
react/features/base/jwt/middleware.js View File

@@ -2,24 +2,15 @@
2 2
 
3 3
 import jwtDecode from 'jwt-decode';
4 4
 
5
-import {
6
-    CONFERENCE_FAILED,
7
-    CONFERENCE_LEFT,
8
-    CONFERENCE_WILL_LEAVE,
9
-    SET_ROOM
10
-} from '../conference';
11 5
 import { SET_CONFIG } from '../config';
12 6
 import { SET_LOCATION_URL } from '../connection';
13
-import { LIB_INIT_ERROR } from '../lib-jitsi-meet';
14 7
 import {
15 8
     getLocalParticipant,
16
-    getParticipantCount,
17
-    PARTICIPANT_JOINED,
18 9
     participantUpdated
19 10
 } from '../participants';
20 11
 import { MiddlewareRegistry } from '../redux';
21 12
 
22
-import { setCalleeInfoVisible, setJWT } from './actions';
13
+import { setJWT } from './actions';
23 14
 import { SET_JWT } from './actionTypes';
24 15
 import { parseJWTFromURLParams } from './functions';
25 16
 
@@ -34,14 +25,6 @@ declare var APP: Object;
34 25
  */
35 26
 MiddlewareRegistry.register(store => next => action => {
36 27
     switch (action.type) {
37
-    case CONFERENCE_FAILED:
38
-    case CONFERENCE_LEFT:
39
-    case CONFERENCE_WILL_LEAVE:
40
-    case LIB_INIT_ERROR:
41
-    case PARTICIPANT_JOINED:
42
-    case SET_ROOM:
43
-        return _maybeSetCalleeInfoVisible(store, next, action);
44
-
45 28
     case SET_CONFIG:
46 29
     case SET_LOCATION_URL:
47 30
         // XXX The JSON Web Token (JWT) is not the only piece of state that we
@@ -58,73 +41,6 @@ MiddlewareRegistry.register(store => next => action => {
58 41
     return next(action);
59 42
 });
60 43
 
61
-/**
62
- * Notifies the feature jwt that a specific {@code action} is being dispatched
63
- * within a specific redux {@code store} which may have an effect on the
64
- * visiblity of (the) {@code CalleeInfo}.
65
- *
66
- * @param {Store} store - The redux store in which the specified {@code action}
67
- * is being dispatched.
68
- * @param {Dispatch} next - The redux dispatch function to dispatch the
69
- * specified {@code action} to the specified {@code store}.
70
- * @param {Action} action - The redux action which is being dispatched in the
71
- * specified {@code store}.
72
- * @private
73
- * @returns {Object} The new state that is the result of the reduction of the
74
- * specified {@code action}.
75
- */
76
-function _maybeSetCalleeInfoVisible({ dispatch, getState }, next, action) {
77
-    const result = next(action);
78
-
79
-    const state = getState();
80
-    const stateFeaturesBaseJWT = state['features/base/jwt'];
81
-    let calleeInfoVisible;
82
-
83
-    if (stateFeaturesBaseJWT.callee) {
84
-        const { conference, leaving, room } = state['features/base/conference'];
85
-
86
-        // XXX The CalleeInfo is to be displayed/visible as soon as possible
87
-        // including even before the conference is joined.
88
-        if (room && (!conference || conference !== leaving)) {
89
-            switch (action.type) {
90
-            case CONFERENCE_FAILED:
91
-            case CONFERENCE_LEFT:
92
-            case CONFERENCE_WILL_LEAVE:
93
-            case LIB_INIT_ERROR:
94
-                // Because the CalleeInfo is to be displayed/visible as soon as
95
-                // possible even before the connection is established and/or the
96
-                // conference is joined, it is very complicated to figure out
97
-                // based on the current state alone. In order to reduce the
98
-                // risks of displaying the CallOverly at inappropirate times, do
99
-                // not even attempt to figure out based on the current state.
100
-                // The (redux) actions listed above are also the equivalents of
101
-                // the execution ponints at which APP.UI.hideRingOverlay() used
102
-                // to be invoked.
103
-                break;
104
-
105
-            default: {
106
-                // The CalleeInfo is to no longer be displayed/visible as soon
107
-                // as another participant joins.
108
-                calleeInfoVisible
109
-                    = getParticipantCount(state) === 1
110
-                        && Boolean(getLocalParticipant(state));
111
-
112
-                // However, the CallDialog is not to be displayed/visible again
113
-                // after all remote participants leave.
114
-                if (calleeInfoVisible
115
-                        && stateFeaturesBaseJWT.calleeInfoVisible === false) {
116
-                    calleeInfoVisible = false;
117
-                }
118
-                break;
119
-            }
120
-            }
121
-        }
122
-    }
123
-    dispatch(setCalleeInfoVisible(calleeInfoVisible));
124
-
125
-    return result;
126
-}
127
-
128 44
 /**
129 45
  * Overwrites the properties {@code avatarURL}, {@code email}, and {@code name}
130 46
  * of the local participant stored in the redux state base/participants.
@@ -248,7 +164,7 @@ function _setJWT(store, next, action) {
248 164
         }
249 165
     }
250 166
 
251
-    return _maybeSetCalleeInfoVisible(store, next, action);
167
+    return next(action);
252 168
 }
253 169
 
254 170
 /**

+ 2
- 14
react/features/base/jwt/reducer.js View File

@@ -1,27 +1,18 @@
1 1
 // @flow
2 2
 
3
-import { equals, set, ReducerRegistry } from '../redux';
3
+import { equals, ReducerRegistry } from '../redux';
4 4
 
5
-import { SET_CALLEE_INFO_VISIBLE, SET_JWT } from './actionTypes';
5
+import { SET_JWT } from './actionTypes';
6 6
 
7 7
 /**
8 8
  * The default/initial redux state of the feature jwt.
9 9
  *
10 10
  * @private
11 11
  * @type {{
12
- *     calleeInfoVisible: ?boolean
13 12
  *     isGuest: boolean
14 13
  * }}
15 14
  */
16 15
 const DEFAULT_STATE = {
17
-    /**
18
-     * The indicator which determines whether (the) {@code CalleeInfo} is
19
-     * visible.
20
-     *
21
-     * @type {boolean|undefined}
22
-     */
23
-    calleeInfoVisible: undefined,
24
-
25 16
     /**
26 17
      * The indicator which determines whether the local participant is a guest
27 18
      * in the conference.
@@ -44,9 +35,6 @@ ReducerRegistry.register(
44 35
     'features/base/jwt',
45 36
     (state = DEFAULT_STATE, action) => {
46 37
         switch (action.type) {
47
-        case SET_CALLEE_INFO_VISIBLE:
48
-            return set(state, 'calleeInfoVisible', action.calleeInfoVisible);
49
-
50 38
         case SET_JWT: {
51 39
             // eslint-disable-next-line no-unused-vars
52 40
             const { type, ...payload } = action;

+ 2
- 0
react/features/base/participants/reducer.js View File

@@ -180,6 +180,7 @@ function _participant(state: Object = {}, action) {
180 180
 function _participantJoined({ participant }) {
181 181
     const {
182 182
         avatarURL,
183
+        botType,
183 184
         connectionStatus,
184 185
         dominantSpeaker,
185 186
         email,
@@ -212,6 +213,7 @@ function _participantJoined({ participant }) {
212 213
     return {
213 214
         avatarID,
214 215
         avatarURL,
216
+        botType,
215 217
         conference,
216 218
         connectionStatus,
217 219
         dominantSpeaker: dominantSpeaker || false,

+ 1
- 1
react/features/conference/components/Conference.native.js View File

@@ -9,7 +9,7 @@ import { connect as reactReduxConnect } from 'react-redux';
9 9
 import { appNavigate } from '../../app';
10 10
 import { connect, disconnect } from '../../base/connection';
11 11
 import { DialogContainer } from '../../base/dialog';
12
-import { CalleeInfoContainer } from '../../base/jwt';
12
+import { CalleeInfoContainer } from '../../invite';
13 13
 import { getParticipantCount } from '../../base/participants';
14 14
 import { Container, LoadingIndicator, TintedView } from '../../base/react';
15 15
 import { TestConnectionInfo } from '../../base/testing';

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

@@ -7,8 +7,8 @@ import { connect as reactReduxConnect } from 'react-redux';
7 7
 import { connect, disconnect } from '../../base/connection';
8 8
 import { DialogContainer } from '../../base/dialog';
9 9
 import { translate } from '../../base/i18n';
10
-import { CalleeInfoContainer } from '../../base/jwt';
11 10
 import { Filmstrip } from '../../filmstrip';
11
+import { CalleeInfoContainer } from '../../invite';
12 12
 import { LargeVideo } from '../../large-video';
13 13
 import { NotificationsContainer } from '../../notifications';
14 14
 import { SidePanel } from '../../side-panel';

+ 4
- 0
react/features/filmstrip/functions.web.js View File

@@ -32,6 +32,10 @@ export function isFilmstripVisible(stateful: Object | Function) {
32 32
  * in the filmstrip, then {@code true}; otherwise, {@code false}.
33 33
  */
34 34
 export function shouldRemoteVideosBeVisible(state: Object) {
35
+    if (state['features/invite'].calleeInfoVisible) {
36
+        return false;
37
+    }
38
+
35 39
     const participantCount = getParticipantCount(state);
36 40
     let pinnedParticipant;
37 41
 

+ 0
- 41
react/features/filmstrip/middleware.js View File

@@ -1,10 +1,8 @@
1 1
 // @flow
2 2
 
3 3
 import { setLastN } from '../base/conference';
4
-import { SET_CALLEE_INFO_VISIBLE } from '../base/jwt';
5 4
 import { pinParticipant } from '../base/participants';
6 5
 import { MiddlewareRegistry } from '../base/redux';
7
-import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
8 6
 
9 7
 import { SET_FILMSTRIP_ENABLED } from './actionTypes';
10 8
 
@@ -12,9 +10,6 @@ declare var APP: Object;
12 10
 
13 11
 MiddlewareRegistry.register(store => next => action => {
14 12
     switch (action.type) {
15
-    case SET_CALLEE_INFO_VISIBLE:
16
-        return _setCalleeInfoVisible(store, next, action);
17
-
18 13
     case SET_FILMSTRIP_ENABLED:
19 14
         return _setFilmstripEnabled(store, next, action);
20 15
     }
@@ -22,42 +17,6 @@ MiddlewareRegistry.register(store => next => action => {
22 17
     return next(action);
23 18
 });
24 19
 
25
-/**
26
- * Notifies the feature filmstrip that the action
27
- * {@link SET_CALLEE_INFO_VISIBLE} is being dispatched within a specific redux
28
- * store.
29
- *
30
- * @param {Store} store - The redux store in which the specified action is being
31
- * dispatched.
32
- * @param {Dispatch} next - The redux dispatch function to dispatch the
33
- * specified action to the specified store.
34
- * @param {Action} action - The redux action {@code SET_CALLEE_INFO_VISIBLE}
35
- * which is being dispatched in the specified store.
36
- * @private
37
- * @returns {Object} The value returned by {@code next(action)}.
38
- */
39
-function _setCalleeInfoVisible({ getState }, next, action) {
40
-    if (typeof APP !== 'undefined') {
41
-        const oldValue
42
-            = Boolean(getState()['features/base/jwt'].calleeInfoVisible);
43
-        const result = next(action);
44
-        const newValue
45
-            = Boolean(getState()['features/base/jwt'].calleeInfoVisible);
46
-
47
-        oldValue === newValue
48
-
49
-            // FIXME The following accesses the private state filmstrip of
50
-            // Filmstrip. It is written with the understanding that Filmstrip
51
-            // will be rewritten in React and, consequently, will not need the
52
-            // middleware implemented here, Filmstrip.init, and UI.start.
53
-            || (Filmstrip.filmstrip && Filmstrip.toggleFilmstrip(!newValue));
54
-
55
-        return result;
56
-    }
57
-
58
-    return next(action);
59
-}
60
-
61 20
 /**
62 21
  * Notifies the feature filmstrip that the action {@link SET_FILMSTRIP_ENABLED}
63 22
  * is being dispatched within a specific redux store.

+ 38
- 6
react/features/invite/actionTypes.js View File

@@ -1,3 +1,27 @@
1
+/**
2
+ * The type of redux action to set the {@code EventEmitter} subscriptions
3
+ * utilized by the feature invite.
4
+ *
5
+ * {
6
+ *     type: _SET_EMITTER_SUBSCRIPTIONS,
7
+ *     emitterSubscriptions: Array|undefined
8
+ * }
9
+ *
10
+ * @protected
11
+ */
12
+export const _SET_EMITTER_SUBSCRIPTIONS = Symbol('_SET_EMITTER_SUBSCRIPTIONS');
13
+
14
+/**
15
+ * The type of redux action which will add pending invite request to the redux
16
+ * store.
17
+ *
18
+ * {
19
+ *     type: ADD_PENDING_INVITE_REQUEST,
20
+ *     request: Object
21
+ * }
22
+ */
23
+export const ADD_PENDING_INVITE_REQUEST = Symbol('ADD_PENDING_INVITE_REQUEST');
24
+
1 25
 /**
2 26
  * The type of the (redux) action which signals that a click/tap has been
3 27
  * performed on {@link InviteButton} and that the execution flow for
@@ -10,17 +34,25 @@
10 34
 export const BEGIN_ADD_PEOPLE = Symbol('BEGIN_ADD_PEOPLE');
11 35
 
12 36
 /**
13
- * The type of redux action to set the {@code EventEmitter} subscriptions
14
- * utilized by the feature invite.
37
+ * The type of redux action which will remove pending invite requests from the
38
+ * redux store.
15 39
  *
16 40
  * {
17
- *     type: _SET_EMITTER_SUBSCRIPTIONS,
18
- *     emitterSubscriptions: Array|undefined
41
+ *     type: REMOVE_PENDING_INVITE_REQUESTS
19 42
  * }
43
+ */
44
+export const REMOVE_PENDING_INVITE_REQUESTS
45
+    = Symbol('REMOVE_PENDING_INVITE_REQUESTS');
46
+
47
+/**
48
+ * The type of redux action which sets the visibility of {@code CalleeInfo}.
20 49
  *
21
- * @protected
50
+ * {
51
+ *     type: SET_CALLEE_INFO_VISIBLE,
52
+ *     calleeInfoVisible: boolean
53
+ * }
22 54
  */
23
-export const _SET_EMITTER_SUBSCRIPTIONS = Symbol('_SET_EMITTER_SUBSCRIPTIONS');
55
+export const SET_CALLEE_INFO_VISIBLE = Symbol('SET_CALLEE_INFO_VISIBLE');
24 56
 
25 57
 /**
26 58
  * The type of the action which signals an error occurred while requesting dial-

+ 110
- 24
react/features/invite/actions.js View File

@@ -2,9 +2,13 @@
2 2
 
3 3
 import { getInviteURL } from '../base/connection';
4 4
 import { inviteVideoRooms } from '../videosipgw';
5
+import { getParticipants } from '../base/participants';
5 6
 
6 7
 import {
8
+    ADD_PENDING_INVITE_REQUEST,
7 9
     BEGIN_ADD_PEOPLE,
10
+    REMOVE_PENDING_INVITE_REQUESTS,
11
+    SET_CALLEE_INFO_VISIBLE,
8 12
     UPDATE_DIAL_IN_NUMBERS_FAILED,
9 13
     UPDATE_DIAL_IN_NUMBERS_SUCCESS
10 14
 } from './actionTypes';
@@ -31,23 +35,51 @@ export function beginAddPeople() {
31 35
     };
32 36
 }
33 37
 
38
+
34 39
 /**
35 40
  * Invites (i.e. sends invites to) an array of invitees (which may be a
36 41
  * combination of users, rooms, phone numbers, and video rooms).
37 42
  *
38 43
  * @param  {Array<Object>} invitees - The recepients to send invites to.
44
+ * @param  {Array<Object>} showCalleeInfo - Indicates whether the
45
+ * {@code CalleeInfo} should be displayed or not.
39 46
  * @returns {Promise<Array<Object>>} A {@code Promise} resolving with an array
40 47
  * of invitees who were not invited (i.e. invites were not sent to them).
41 48
  */
42
-export function invite(invitees: Array<Object>) {
49
+export function invite(
50
+        invitees: Array<Object>,
51
+        showCalleeInfo: boolean = false) {
43 52
     return (
44 53
             dispatch: Dispatch<*>,
45 54
             getState: Function): Promise<Array<Object>> => {
55
+        const state = getState();
56
+        const participants = getParticipants(state);
57
+        const { calleeInfoVisible } = state['features/invite'];
58
+
59
+        if (showCalleeInfo
60
+                && !calleeInfoVisible
61
+                && invitees.length === 1
62
+                && invitees[0].type === 'user'
63
+                && participants.length === 1) {
64
+            dispatch(setCalleeInfoVisible(true, invitees[0]));
65
+        }
66
+
67
+        const { conference } = state['features/base/conference'];
68
+
69
+        if (typeof conference === 'undefined') {
70
+            // Invite will fail before CONFERENCE_JOIN. The request will be
71
+            // cached in order to be executed on CONFERENCE_JOIN.
72
+            return new Promise(resolve => {
73
+                dispatch(addPendingInviteRequest({
74
+                    invitees,
75
+                    callback: failedInvitees => resolve(failedInvitees)
76
+                }));
77
+            });
78
+        }
79
+
46 80
         let allInvitePromises = [];
47 81
         let invitesLeftToSend = [ ...invitees ];
48 82
 
49
-        const state = getState();
50
-        const { conference } = state['features/base/conference'];
51 83
         const {
52 84
             callFlowsEnabled,
53 85
             inviteServiceUrl,
@@ -57,27 +89,25 @@ export function invite(invitees: Array<Object>) {
57 89
         const { jwt } = state['features/base/jwt'];
58 90
 
59 91
         // First create all promises for dialing out.
60
-        if (conference) {
61
-            const phoneNumbers
62
-                = invitesLeftToSend.filter(({ type }) => type === 'phone');
63
-
64
-            // For each number, dial out. On success, remove the number from
65
-            // {@link invitesLeftToSend}.
66
-            const phoneInvitePromises = phoneNumbers.map(item => {
67
-                const numberToInvite = item.number;
68
-
69
-                return conference.dial(numberToInvite)
70
-                    .then(() => {
71
-                        invitesLeftToSend
72
-                            = invitesLeftToSend.filter(
73
-                                invitee => invitee !== item);
74
-                    })
75
-                    .catch(error =>
76
-                        logger.error('Error inviting phone number:', error));
77
-            });
92
+        const phoneNumbers
93
+            = invitesLeftToSend.filter(({ type }) => type === 'phone');
78 94
 
79
-            allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
80
-        }
95
+        // For each number, dial out. On success, remove the number from
96
+        // {@link invitesLeftToSend}.
97
+        const phoneInvitePromises = phoneNumbers.map(item => {
98
+            const numberToInvite = item.number;
99
+
100
+            return conference.dial(numberToInvite)
101
+                .then(() => {
102
+                    invitesLeftToSend
103
+                        = invitesLeftToSend.filter(
104
+                            invitee => invitee !== item);
105
+                })
106
+                .catch(error =>
107
+                    logger.error('Error inviting phone number:', error));
108
+        });
109
+
110
+        allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
81 111
 
82 112
         const usersAndRooms
83 113
             = invitesLeftToSend.filter(
@@ -98,7 +128,10 @@ export function invite(invitees: Array<Object>) {
98 128
                         = invitesLeftToSend.filter(
99 129
                             ({ type }) => type !== 'user' && type !== 'room');
100 130
                 })
101
-                .catch(error => logger.error('Error inviting people:', error));
131
+                .catch(error => {
132
+                    dispatch(setCalleeInfoVisible(false));
133
+                    logger.error('Error inviting people:', error);
134
+                });
102 135
 
103 136
             allInvitePromises.push(peopleInvitePromise);
104 137
         }
@@ -163,3 +196,56 @@ export function updateDialInNumbers() {
163 196
             });
164 197
     };
165 198
 }
199
+
200
+/**
201
+ * Sets the visibility of {@code CalleeInfo}.
202
+ *
203
+ * @param {boolean|undefined} [calleeInfoVisible] - If {@code CalleeInfo} is
204
+ * to be displayed/visible, then {@code true}; otherwise, {@code false} or
205
+ * {@code undefined}.
206
+ * @param {Object|undefined} [initialCalleeInfo] - Callee information.
207
+ * @returns {{
208
+ *     type: SET_CALLEE_INFO_VISIBLE,
209
+ *     calleeInfoVisible: (boolean|undefined),
210
+ *     initialCalleeInfo
211
+ * }}
212
+ */
213
+export function setCalleeInfoVisible(
214
+        calleeInfoVisible: boolean,
215
+        initialCalleeInfo: ?Object) {
216
+    return {
217
+        type: SET_CALLEE_INFO_VISIBLE,
218
+        calleeInfoVisible,
219
+        initialCalleeInfo
220
+    };
221
+}
222
+
223
+/**
224
+ * Adds pending invite request.
225
+ *
226
+ * @param {Object} request - The request.
227
+ * @returns {{
228
+ *     type: ADD_PENDING_INVITE_REQUEST,
229
+ *     request: Object
230
+ * }}
231
+ */
232
+export function addPendingInviteRequest(
233
+        request: { invitees: Array<Object>, callback: Function }) {
234
+    return {
235
+        type: ADD_PENDING_INVITE_REQUEST,
236
+        request
237
+    };
238
+}
239
+
240
+/**
241
+ * Removes all pending invite requests.
242
+ *
243
+ * @returns {{
244
+ *     type: REMOVE_PENDING_INVITE_REQUEST
245
+ * }}
246
+ */
247
+export function removePendingInviteRequests() {
248
+    return {
249
+        type: REMOVE_PENDING_INVITE_REQUESTS
250
+    };
251
+}

+ 161
- 0
react/features/invite/components/callee-info/CalleeInfo.js View File

@@ -0,0 +1,161 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+import { MEDIA_TYPE } from '../../../base/media';
7
+import {
8
+    Avatar,
9
+    getAvatarURL,
10
+    getParticipants,
11
+    getParticipantDisplayName,
12
+    getParticipantPresenceStatus
13
+} from '../../../base/participants';
14
+import { Container, Text } from '../../../base/react';
15
+import { isLocalTrackMuted } from '../../../base/tracks';
16
+import { CALLING, PresenceLabel } from '../../../presence-status';
17
+
18
+import styles from './styles';
19
+
20
+/**
21
+ * The type of the React {@code Component} props of {@link CalleeInfo}.
22
+ */
23
+type Props = {
24
+
25
+    /**
26
+     * The callee's information such as avatar and display name.
27
+     */
28
+    _callee: Object,
29
+
30
+    _isVideoMuted: boolean
31
+};
32
+
33
+
34
+/**
35
+ * Implements a React {@link Component} which depicts the establishment of a
36
+ * call with a specific remote callee.
37
+ *
38
+ * @extends Component
39
+ */
40
+class CalleeInfo extends Component<Props> {
41
+    /**
42
+     * Implements React's {@link Component#render()}.
43
+     *
44
+     * @inheritdoc
45
+     * @returns {ReactElement}
46
+     */
47
+    render() {
48
+        const {
49
+            avatar,
50
+            name,
51
+            status = CALLING
52
+        } = this.props._callee;
53
+        const className = this.props._isVideoMuted ? 'solidBG' : undefined;
54
+
55
+        return (
56
+            <Container
57
+                { ...this._style('ringing', className) }
58
+                id = 'ringOverlay'>
59
+                <Container
60
+                    { ...this._style('ringing__content') }>
61
+                    <Avatar
62
+                        { ...this._style('ringing__avatar') }
63
+                        uri = { avatar } />
64
+                    <Container { ...this._style('ringing__status') }>
65
+                        <PresenceLabel
66
+                            defaultPresence = { status }
67
+                            styles = { this._style('ringing__text') } />
68
+                    </Container>
69
+                    <Container { ...this._style('ringing__name') }>
70
+                        <Text
71
+                            { ...this._style('ringing__text') }>
72
+                            { name }
73
+                        </Text>
74
+                    </Container>
75
+                </Container>
76
+            </Container>
77
+        );
78
+    }
79
+
80
+    /**
81
+     * Attempts to convert specified CSS class names into React
82
+     * {@link Component} props {@code style} or {@code className}.
83
+     *
84
+     * @param {Array<string>} classNames - The CSS class names to convert
85
+     * into React {@code Component} props {@code style} or {@code className}.
86
+     * @returns {{
87
+     *     className: string,
88
+     *     style: Object
89
+     * }}
90
+     */
91
+    _style(...classNames: Array<?string>) {
92
+        let className = '';
93
+        let style;
94
+
95
+        for (const aClassName of classNames) {
96
+            if (aClassName) {
97
+                // Attemp to convert aClassName into style.
98
+                if (styles && aClassName in styles) {
99
+                    // React Native will accept an Array as the value of the
100
+                    // style prop. However, I do not know about React.
101
+                    style = {
102
+                        ...style,
103
+                        ...styles[aClassName]
104
+                    };
105
+                } else {
106
+                    // Otherwise, leave it as className.
107
+                    className += `${aClassName} `;
108
+                }
109
+            }
110
+        }
111
+
112
+        // Choose which of the className and/or style props has a value and,
113
+        // consequently, must be returned.
114
+        const props = {};
115
+
116
+        if (className) {
117
+            props.className = className.trim();
118
+        }
119
+        if (style) {
120
+            props.style = style;
121
+        }
122
+
123
+        return props;
124
+    }
125
+}
126
+
127
+/**
128
+ * Maps (parts of) the redux state to {@code CalleeInfo}'s props.
129
+ *
130
+ * @param {Object} state - The redux state.
131
+ * @private
132
+ * @returns {{
133
+ *     _callee: Object
134
+ * }}
135
+ */
136
+function _mapStateToProps(state) {
137
+    const _isVideoMuted
138
+        = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
139
+    const poltergeist
140
+        = getParticipants(state).find(p => p.botType === 'poltergeist');
141
+
142
+    if (poltergeist) {
143
+        const { id } = poltergeist;
144
+
145
+        return {
146
+            _callee: {
147
+                avatar: getAvatarURL(poltergeist),
148
+                name: getParticipantDisplayName(state, id),
149
+                status: getParticipantPresenceStatus(state, id)
150
+            },
151
+            _isVideoMuted
152
+        };
153
+    }
154
+
155
+    return {
156
+        _callee: state['features/invite'].initialCalleeInfo,
157
+        _isVideoMuted
158
+    };
159
+}
160
+
161
+export default connect(_mapStateToProps)(CalleeInfo);

react/features/base/jwt/components/CalleeInfoContainer.js → react/features/invite/components/callee-info/CalleeInfoContainer.js View File

@@ -57,7 +57,7 @@ function _mapStateToProps(state: Object): Object {
57 57
          * @private
58 58
          * @type {boolean}
59 59
          */
60
-        _calleeInfoVisible: state['features/base/jwt'].calleeInfoVisible
60
+        _calleeInfoVisible: state['features/invite'].calleeInfoVisible
61 61
     };
62 62
 }
63 63
 

react/features/base/jwt/components/index.js → react/features/invite/components/callee-info/index.js View File

@@ -1,2 +1 @@
1
-export { default as CalleeInfo } from './CalleeInfo';
2 1
 export { default as CalleeInfoContainer } from './CalleeInfoContainer';

react/features/base/jwt/components/styles.native.js → react/features/invite/components/callee-info/styles.native.js View File

@@ -1,6 +1,6 @@
1 1
 import { StyleSheet } from 'react-native';
2 2
 
3
-import { ColorPalette, createStyleSheet } from '../../styles';
3
+import { ColorPalette, createStyleSheet } from '../../../base/styles';
4 4
 
5 5
 export default createStyleSheet({
6 6
     // XXX The names bellow were preserved for the purposes of compatibility

react/features/base/jwt/components/styles.web.js → react/features/invite/components/callee-info/styles.web.js View File


+ 1
- 0
react/features/invite/components/index.js View File

@@ -2,3 +2,4 @@ export { default as AddPeopleDialog } from './AddPeopleDialog';
2 2
 export { DialInSummary } from './dial-in-summary';
3 3
 export { default as InfoDialogButton } from './InfoDialogButton';
4 4
 export { default as InviteButton } from './InviteButton';
5
+export * from './callee-info';

+ 86
- 12
react/features/invite/middleware.any.js View File

@@ -2,11 +2,17 @@
2 2
 
3 3
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
4 4
 import {
5
+    CONFERENCE_JOINED
6
+} from '../base/conference';
7
+import {
8
+    getLocalParticipant,
5 9
     getParticipantPresenceStatus,
10
+    getParticipants,
6 11
     PARTICIPANT_JOINED,
7 12
     PARTICIPANT_JOINED_SOUND_ID,
8 13
     PARTICIPANT_LEFT,
9
-    PARTICIPANT_UPDATED
14
+    PARTICIPANT_UPDATED,
15
+    pinParticipant
10 16
 } from '../base/participants';
11 17
 import { MiddlewareRegistry } from '../base/redux';
12 18
 import {
@@ -24,7 +30,15 @@ import {
24 30
     RINGING
25 31
 } from '../presence-status';
26 32
 
27
-import { UPDATE_DIAL_IN_NUMBERS_FAILED } from './actionTypes';
33
+import {
34
+    SET_CALLEE_INFO_VISIBLE,
35
+    UPDATE_DIAL_IN_NUMBERS_FAILED
36
+} from './actionTypes';
37
+import {
38
+    invite,
39
+    removePendingInviteRequests,
40
+    setCalleeInfoVisible
41
+} from './actions';
28 42
 import {
29 43
     OUTGOING_CALL_EXPIRED_SOUND_ID,
30 44
     OUTGOING_CALL_REJECTED_SOUND_ID,
@@ -59,13 +73,22 @@ const statusToRingtone = {
59 73
  */
60 74
 MiddlewareRegistry.register(store => next => action => {
61 75
     let oldParticipantPresence;
76
+    const { dispatch, getState } = store;
77
+    const state = getState();
62 78
 
63 79
     if (action.type === PARTICIPANT_UPDATED
64 80
         || action.type === PARTICIPANT_LEFT) {
65 81
         oldParticipantPresence
66
-            = getParticipantPresenceStatus(
67
-                store.getState(),
68
-                action.participant.id);
82
+            = getParticipantPresenceStatus(state, action.participant.id);
83
+    }
84
+
85
+    if (action.type === SET_CALLEE_INFO_VISIBLE) {
86
+        if (action.calleeInfoVisible) {
87
+            dispatch(pinParticipant(getLocalParticipant(state).id));
88
+        } else {
89
+            // unpin participant
90
+            dispatch(pinParticipant());
91
+        }
69 92
     }
70 93
 
71 94
     const result = next(action);
@@ -73,23 +96,27 @@ MiddlewareRegistry.register(store => next => action => {
73 96
     switch (action.type) {
74 97
     case APP_WILL_MOUNT:
75 98
         for (const [ soundId, sound ] of sounds.entries()) {
76
-            store.dispatch(registerSound(soundId, sound.file, sound.options));
99
+            dispatch(registerSound(soundId, sound.file, sound.options));
77 100
         }
78 101
         break;
79 102
 
80 103
     case APP_WILL_UNMOUNT:
81 104
         for (const soundId of sounds.keys()) {
82
-            store.dispatch(unregisterSound(soundId));
105
+            dispatch(unregisterSound(soundId));
83 106
         }
84 107
         break;
85 108
 
109
+    case CONFERENCE_JOINED:
110
+        _onConferenceJoined(store);
111
+        break;
112
+
86 113
     case PARTICIPANT_JOINED:
87 114
     case PARTICIPANT_LEFT:
88 115
     case PARTICIPANT_UPDATED: {
116
+        _maybeHideCalleeInfo(action, store);
117
+
89 118
         const newParticipantPresence
90
-            = getParticipantPresenceStatus(
91
-                store.getState(),
92
-                action.participant.id);
119
+            = getParticipantPresenceStatus(state, action.participant.id);
93 120
 
94 121
         if (oldParticipantPresence === newParticipantPresence) {
95 122
             break;
@@ -108,11 +135,11 @@ MiddlewareRegistry.register(store => next => action => {
108 135
         }
109 136
 
110 137
         if (oldSoundId) {
111
-            store.dispatch(stopSound(oldSoundId));
138
+            dispatch(stopSound(oldSoundId));
112 139
         }
113 140
 
114 141
         if (newSoundId) {
115
-            store.dispatch(playSound(newSoundId));
142
+            dispatch(playSound(newSoundId));
116 143
         }
117 144
 
118 145
         break;
@@ -126,3 +153,50 @@ MiddlewareRegistry.register(store => next => action => {
126 153
 
127 154
     return result;
128 155
 });
156
+
157
+/**
158
+ * Hides the callee info layot if there are more than 1 real
159
+ * (not poltergeist, shared video, etc.) participants in the call.
160
+ *
161
+ * @param {Object} action - The redux action.
162
+ * @param {ReduxStore} store - The redux store.
163
+ * @returns {void}
164
+ */
165
+function _maybeHideCalleeInfo(action, store) {
166
+    const state = store.getState();
167
+
168
+    if (!state['features/invite'].calleeInfoVisible) {
169
+        return;
170
+    }
171
+    const participants = getParticipants(state);
172
+    const numberOfPoltergeists
173
+        = participants.filter(p => p.botType === 'poltergeist').length;
174
+    const numberOfRealParticipants = participants.length - numberOfPoltergeists;
175
+
176
+    if ((numberOfPoltergeists > 1 || numberOfRealParticipants > 1)
177
+        || (action.type === PARTICIPANT_LEFT && participants.length === 1)) {
178
+        store.dispatch(setCalleeInfoVisible(false));
179
+    }
180
+}
181
+
182
+/**
183
+ * Executes the pending invitation requests if any.
184
+ *
185
+ * @param {ReduxStore} store - The redux store.
186
+ * @returns {void}
187
+ */
188
+function _onConferenceJoined(store) {
189
+    const { dispatch, getState } = store;
190
+
191
+    const pendingInviteRequests
192
+        = getState()['features/invite'].pendingInviteRequests || [];
193
+
194
+    pendingInviteRequests.forEach(({ invitees, callback }) => {
195
+        dispatch(invite(invitees))
196
+            .then(failedInvitees => {
197
+                callback(failedInvitees);
198
+            });
199
+    });
200
+
201
+    dispatch(removePendingInviteRequests());
202
+}

+ 33
- 1
react/features/invite/reducer.js View File

@@ -4,12 +4,24 @@ import { assign, ReducerRegistry } from '../base/redux';
4 4
 
5 5
 import {
6 6
     _SET_EMITTER_SUBSCRIPTIONS,
7
+    ADD_PENDING_INVITE_REQUEST,
8
+    REMOVE_PENDING_INVITE_REQUESTS,
9
+    SET_CALLEE_INFO_VISIBLE,
7 10
     UPDATE_DIAL_IN_NUMBERS_FAILED,
8 11
     UPDATE_DIAL_IN_NUMBERS_SUCCESS
9 12
 } from './actionTypes';
10 13
 
11 14
 const DEFAULT_STATE = {
12
-    numbersEnabled: true
15
+    /**
16
+     * The indicator which determines whether (the) {@code CalleeInfo} is
17
+     * visible.
18
+     *
19
+     * @type {boolean|undefined}
20
+     */
21
+    calleeInfoVisible: false,
22
+
23
+    numbersEnabled: true,
24
+    pendingInviteRequests: []
13 25
 };
14 26
 
15 27
 ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
@@ -17,6 +29,26 @@ ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => {
17 29
     case _SET_EMITTER_SUBSCRIPTIONS:
18 30
         return (
19 31
             assign(state, 'emitterSubscriptions', action.emitterSubscriptions));
32
+    case ADD_PENDING_INVITE_REQUEST:
33
+        return {
34
+            ...state,
35
+            pendingInviteRequests: [
36
+                ...state.pendingInviteRequests,
37
+                action.request
38
+            ]
39
+        };
40
+    case REMOVE_PENDING_INVITE_REQUESTS:
41
+        return {
42
+            ...state,
43
+            pendingInviteRequests: []
44
+        };
45
+
46
+    case SET_CALLEE_INFO_VISIBLE:
47
+        return {
48
+            ...state,
49
+            calleeInfoVisible: action.calleeInfoVisible,
50
+            initialCalleeInfo: action.initialCalleeInfo
51
+        };
20 52
 
21 53
     case UPDATE_DIAL_IN_NUMBERS_FAILED:
22 54
         return {

+ 3
- 1
react/features/notifications/components/AbstractNotificationsContainer.js View File

@@ -139,8 +139,10 @@ export default class AbstractNotificationsContainer<P: Props>
139 139
 export function _abstractMapStateToProps(state: Object) {
140 140
     const isAnyOverlayVisible = Boolean(getOverlayToRender(state));
141 141
     const { enabled, notifications } = state['features/notifications'];
142
+    const { calleeInfoVisible } = state['features/invite'];
142 143
 
143 144
     return {
144
-        _notifications: enabled && !isAnyOverlayVisible ? notifications : []
145
+        _notifications: enabled && !isAnyOverlayVisible && !calleeInfoVisible
146
+            ? notifications : []
145 147
     };
146 148
 }

+ 23
- 6
react/features/presence-status/components/PresenceLabel.js View File

@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
4 4
 
5 5
 import { translate } from '../../base/i18n';
6 6
 import { getParticipantById } from '../../base/participants';
7
+import { Text } from '../../base/react';
7 8
 
8 9
 import { STATUS_TO_I18N_KEY } from '../constants';
9 10
 
@@ -35,11 +36,27 @@ class PresenceLabel extends Component {
35 36
          */
36 37
         _presence: PropTypes.string,
37 38
 
39
+        /**
40
+         * Default presence status that will be displayed if user's presence
41
+         * status is not available.
42
+         */
43
+        defaultPresence: PropTypes.string,
44
+
45
+        /**
46
+         * Styles for the case where there isn't any content to be shown.
47
+         */
48
+        noContentStyles: PropTypes.object,
49
+
38 50
         /**
39 51
          * The ID of the participant whose presence status shoul display.
40 52
          */
41 53
         participantID: PropTypes.string,
42 54
 
55
+        /**
56
+         * Styles for the presence label.
57
+         */
58
+        styles: PropTypes.object,
59
+
43 60
         /**
44 61
          * Invoked to obtain translated strings.
45 62
          */
@@ -53,14 +70,13 @@ class PresenceLabel extends Component {
53 70
      * @returns {ReactElement}
54 71
      */
55 72
     render() {
56
-        const { _presence } = this.props;
73
+        const { _presence, styles, noContentStyles } = this.props;
74
+        const combinedStyles = _presence ? styles : noContentStyles;
57 75
 
58 76
         return (
59
-            <div
60
-                className
61
-                    = { `presence-label ${_presence ? '' : 'no-presence'}` }>
77
+            <Text { ...combinedStyles }>
62 78
                 { this._getPresenceText() }
63
-            </div>
79
+            </Text>
64 80
         );
65 81
     }
66 82
 
@@ -102,7 +118,8 @@ function _mapStateToProps(state, ownProps) {
102 118
     const participant = getParticipantById(state, ownProps.participantID);
103 119
 
104 120
     return {
105
-        _presence: participant && participant.presence
121
+        _presence:
122
+            (participant && participant.presence) || ownProps.defaultPresence
106 123
     };
107 124
 }
108 125
 

+ 1
- 0
react/features/presence-status/constants.js View File

@@ -122,5 +122,6 @@ export const STATUS_TO_I18N_KEY = {
122 122
     [CONNECTING]: 'presenceStatus.connecting',
123 123
     [CONNECTING2]: 'presenceStatus.connecting2',
124 124
     [CONNECTED_PHONE_NUMBER]: 'presenceStatus.connected',
125
+    [CONNECTED_USER]: 'presenceStatus.connected',
125 126
     [DISCONNECTED]: 'presenceStatus.disconnected'
126 127
 };

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

@@ -89,7 +89,7 @@ export function hideToolbox(force: boolean = false): Function {
89 89
 
90 90
         if (!force
91 91
                 && (hovered
92
-                    || state['features/base/jwt'].calleeInfoVisible
92
+                    || state['features/invite'].calleeInfoVisible
93 93
                     || SideContainerToggler.isVisible())) {
94 94
             dispatch(
95 95
                 setToolboxTimeout(

Loading…
Cancel
Save