Sfoglia il codice sorgente

feat(presence): display status in thumbnail and large video (#1828)

* feat(presence): display status in thumbnail and large video

- Create a React Component for displaying presence. It currently
  connects to the store for participant updates but in the future
  should not be as smart once more reactification occurs.
- Modify filmstrip css so the presence status displays horizontal
  center and below the avatar.
- Modify videolayout css so the presence status displays horizontal
  centered and with a rounded background.
- Dispatch presence updates so the participant state can be update.
- Update message position on large video update to ensure message
  positioning is correct.

* squash: do not show presence message if connection message is displayed
master
virtuacoplenny 8 anni fa
parent
commit
c04ef05058

+ 3
- 0
conference.js Vedi File

@@ -44,6 +44,7 @@ import {
44 44
     participantConnectionStatusChanged,
45 45
     participantJoined,
46 46
     participantLeft,
47
+    participantPresenceChanged,
47 48
     participantRoleChanged,
48 49
     participantUpdated
49 50
 } from './react/features/base/participants';
@@ -1627,6 +1628,8 @@ export default {
1627 1628
         });
1628 1629
 
1629 1630
         room.on(ConferenceEvents.USER_STATUS_CHANGED, (id, status) => {
1631
+            APP.store.dispatch(participantPresenceChanged(id, status));
1632
+
1630 1633
             let user = room.getParticipantById(id);
1631 1634
             if (user) {
1632 1635
                 APP.UI.updateUserStatus(user, status);

+ 18
- 0
css/_filmstrip.scss Vedi File

@@ -103,6 +103,24 @@
103 103
                 display: none;
104 104
             }
105 105
 
106
+            .presence-label {
107
+                color: $participantNameColor;
108
+                font-size: 12px;
109
+                font-weight: 100;
110
+                left: 0;
111
+                margin: 0 auto;
112
+                overflow: hidden;
113
+                pointer-events: none;
114
+                position: absolute;
115
+                right: 0;
116
+                text-align: center;
117
+                text-overflow: ellipsis;
118
+                top: calc(50% + 30px);
119
+                white-space: nowrap;
120
+                width: 100%;
121
+                z-index: $zindex3;
122
+            }
123
+
106 124
             /**
107 125
              * Hovered video thumbnail.
108 126
              */

+ 10
- 1
css/_videolayout_default.scss Vedi File

@@ -487,8 +487,8 @@
487 487
     filter: grayscale(100%);
488 488
 }
489 489
 
490
+#remotePresenceMessage,
490 491
 #remoteConnectionMessage {
491
-    display: none;
492 492
     position: absolute;
493 493
     width: auto;
494 494
     z-index: $zindex2;
@@ -496,6 +496,11 @@
496 496
     font-size: 14px;
497 497
     text-align: center;
498 498
     color: #FFF;
499
+    left: 50%;
500
+    transform: translate(-50%, 0);
501
+}
502
+#remotePresenceMessage .presence-label,
503
+#remoteConnectionMessage {
499 504
     opacity: .80;
500 505
     text-shadow:    0px 0px 1px rgba(0,0,0,0.3),
501 506
                     0px 1px 1px rgba(0,0,0,0.3),
@@ -508,6 +513,10 @@
508 513
     padding-left: 10px;
509 514
     padding-right: 10px;
510 515
 }
516
+#remotePresenceMessage .no-presence,
517
+#remoteConnectionMessage {
518
+    display: none;
519
+}
511 520
 
512 521
 #localConnectionMessage {
513 522
     display: none;

+ 62
- 4
modules/UI/videolayout/LargeVideoManager.js Vedi File

@@ -1,4 +1,12 @@
1 1
 /* global $, APP, config, JitsiMeetJS */
2
+/* eslint-disable no-unused-vars */
3
+import React from 'react';
4
+import ReactDOM from 'react-dom';
5
+import { Provider } from 'react-redux';
6
+
7
+import { PresenceLabel } from '../../../react/features/presence-status';
8
+/* eslint-enable no-unused-vars */
9
+
2 10
 const logger = require("jitsi-meet-logger").getLogger(__filename);
3 11
 
4 12
 import { setLargeVideoHDStatus } from '../../../react/features/base/conference';
@@ -101,8 +109,8 @@ export default class LargeVideoManager {
101 109
     }
102 110
 
103 111
     /**
104
-     * Stops any polling intervals on the instance and and removes any
105
-     * listeners registered on child components.
112
+     * Stops any polling intervals on the instance and removes any
113
+     * listeners registered on child components, including React Components.
106 114
      *
107 115
      * @returns {void}
108 116
      */
@@ -110,6 +118,8 @@ export default class LargeVideoManager {
110 118
         window.clearInterval(this._updateVideoResolutionInterval);
111 119
         this.videoContainer.removeResizeListener(
112 120
             this._onVideoResolutionUpdate);
121
+
122
+        this.removePresenceLabel();
113 123
     }
114 124
 
115 125
     onHoverIn (e) {
@@ -252,6 +262,11 @@ export default class LargeVideoManager {
252 262
                     !overrideAndHide && isConnectionInterrupted,
253 263
                     !overrideAndHide && messageKey);
254 264
 
265
+            // Change the participant id the presence label is listening to.
266
+            this.updatePresenceLabel(id);
267
+
268
+            this.videoContainer.positionRemoteStatusMessages();
269
+
255 270
             // resolve updateLargeVideo promise after everything is done
256 271
             promise.then(resolve);
257 272
 
@@ -385,6 +400,51 @@ export default class LargeVideoManager {
385 400
         AudioLevels.updateLargeVideoAudioLevel("dominantSpeaker", lvl);
386 401
     }
387 402
 
403
+    /**
404
+     * Displays a message of the passed in participant id's presence status. The
405
+     * message will not display if the remote connection message is displayed.
406
+     *
407
+     * @param {string} id - The participant ID whose associated user's presence
408
+     * status should be displayed.
409
+     * @returns {void}
410
+     */
411
+    updatePresenceLabel(id) {
412
+        const isConnectionMessageVisible
413
+            = $('#remoteConnectionMessage').is(':visible');
414
+
415
+        if (isConnectionMessageVisible) {
416
+            this.removePresenceLabel();
417
+            return;
418
+        }
419
+
420
+        const presenceLabelContainer = $('#remotePresenceMessage');
421
+
422
+        if (presenceLabelContainer.length) {
423
+            /* jshint ignore:start */
424
+            ReactDOM.render(
425
+                <Provider store = { APP.store }>
426
+                    <PresenceLabel participantID = { id } />
427
+                </Provider>,
428
+                presenceLabelContainer.get(0));
429
+            /* jshint ignore:end */
430
+        }
431
+    }
432
+
433
+    /**
434
+     * Removes the messages about the displayed participant's presence status.
435
+     *
436
+     * @returns {void}
437
+     */
438
+    removePresenceLabel() {
439
+        const presenceLabelContainer = $('#remotePresenceMessage');
440
+
441
+        if (presenceLabelContainer.length) {
442
+            /* jshint ignore:start */
443
+            ReactDOM.unmountComponentAtNode(presenceLabelContainer.get(0));
444
+            /* jshint ignore:end */
445
+        }
446
+    }
447
+
388 448
     /**
389 449
      * Show or hide watermark.
390 450
      * @param {boolean} show
@@ -463,8 +523,6 @@ export default class LargeVideoManager {
463 523
             APP.translation.translateElement(
464 524
                 $('#remoteConnectionMessage'), msgOptions);
465 525
         }
466
-
467
-        this.videoContainer.positionRemoteConnectionMessage();
468 526
     }
469 527
 
470 528
     /**

+ 45
- 0
modules/UI/videolayout/RemoteVideo.js Vedi File

@@ -3,7 +3,9 @@
3 3
 /* eslint-disable no-unused-vars */
4 4
 import React from 'react';
5 5
 import ReactDOM from 'react-dom';
6
+import { Provider } from 'react-redux';
6 7
 
8
+import { PresenceLabel } from '../../../react/features/presence-status';
7 9
 import {
8 10
     MuteButton,
9 11
     KickButton,
@@ -100,6 +102,8 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
100 102
 
101 103
     this.addAudioLevelIndicator();
102 104
 
105
+    this.addPresenceLabel();
106
+
103 107
     return this.container;
104 108
 };
105 109
 
@@ -530,6 +534,8 @@ RemoteVideo.prototype.remove = function () {
530 534
 
531 535
     this.removeAvatar();
532 536
 
537
+    this.removePresenceLabel();
538
+
533 539
     this._unmountIndicators();
534 540
 
535 541
     // Make sure that the large video is updated if are removing its
@@ -666,6 +672,41 @@ RemoteVideo.prototype.removeRemoteVideoMenu = function() {
666 672
     }
667 673
 };
668 674
 
675
+/**
676
+ * Mounts the {@code PresenceLabel} for displaying the participant's current
677
+ * presence status.
678
+ *
679
+ * @return {void}
680
+ */
681
+RemoteVideo.prototype.addPresenceLabel = function () {
682
+    const presenceLabelContainer
683
+        = this.container.querySelector('.presence-label-container');
684
+
685
+    if (presenceLabelContainer) {
686
+        /* jshint ignore:start */
687
+        ReactDOM.render(
688
+            <Provider store = { APP.store }>
689
+                <PresenceLabel participantID = { this.id } />
690
+            </Provider>,
691
+            presenceLabelContainer);
692
+        /* jshint ignore:end */
693
+    }
694
+};
695
+
696
+/**
697
+ * Unmounts the {@code PresenceLabel} component.
698
+ *
699
+ * @return {void}
700
+ */
701
+RemoteVideo.prototype.removePresenceLabel = function () {
702
+    const presenceLabelContainer
703
+        = this.container.querySelector('.presence-label-container');
704
+
705
+    if (presenceLabelContainer) {
706
+        ReactDOM.unmountComponentAtNode(presenceLabelContainer);
707
+    }
708
+};
709
+
669 710
 RemoteVideo.createContainer = function (spanId) {
670 711
     let container = document.createElement('span');
671 712
     container.id = spanId;
@@ -695,6 +736,10 @@ RemoteVideo.createContainer = function (spanId) {
695 736
     avatarContainer.className = 'avatar-container';
696 737
     container.appendChild(avatarContainer);
697 738
 
739
+    const presenceLabelContainer = document.createElement('div');
740
+    presenceLabelContainer.className = 'presence-label-container';
741
+    container.appendChild(presenceLabelContainer);
742
+
698 743
     var remotes = document.getElementById('filmstripRemoteVideosContainer');
699 744
     return remotes.appendChild(container);
700 745
 };

+ 24
- 14
modules/UI/videolayout/VideoContainer.js Vedi File

@@ -189,6 +189,8 @@ export class VideoContainer extends LargeContainer {
189 189
          */
190 190
         this.$remoteConnectionMessage = $('#remoteConnectionMessage');
191 191
 
192
+        this.$remotePresenceMessage = $('#remotePresenceMessage');
193
+
192 194
         /**
193 195
          * Indicates whether or not the video stream attached to the video
194 196
          * element has started(which means that there is any image rendered
@@ -321,27 +323,35 @@ export class VideoContainer extends LargeContainer {
321 323
     }
322 324
 
323 325
     /**
324
-     * Update position of the remote connection message which describes that
325
-     * the remote user is having connectivity issues.
326
+     * Updates the positioning of the remote connection presence message and the
327
+     * connection status message which escribes that the remote user is having
328
+     * connectivity issues.
329
+     *
330
+     * @returns {void}
326 331
      */
327
-    positionRemoteConnectionMessage () {
332
+    positionRemoteStatusMessages() {
333
+        this._positionParticipantStatus(this.$remoteConnectionMessage);
334
+        this._positionParticipantStatus(this.$remotePresenceMessage);
335
+    }
328 336
 
337
+    /**
338
+     * Modifies the position of the passed in jQuery object so it displays
339
+     * in the middle of the video container or below the avatar.
340
+     *
341
+     * @private
342
+     * @returns {void}
343
+     */
344
+    _positionParticipantStatus($element) {
329 345
         if (this.avatarDisplayed) {
330 346
             let $avatarImage = $("#dominantSpeakerAvatar");
331
-            this.$remoteConnectionMessage.css(
347
+            $element.css(
332 348
                 'top',
333 349
                 $avatarImage.offset().top + $avatarImage.height() + 10);
334 350
         } else {
335
-            let height = this.$remoteConnectionMessage.height();
336
-            let parentHeight = this.$remoteConnectionMessage.parent().height();
337
-            this.$remoteConnectionMessage.css(
338
-                'top', (parentHeight/2) - (height/2));
351
+            let height = $element.height();
352
+            let parentHeight = $element.parent().height();
353
+            $element.css('top', (parentHeight/2) - (height/2));
339 354
         }
340
-
341
-        let width = this.$remoteConnectionMessage.width();
342
-        let parentWidth = this.$remoteConnectionMessage.parent().width();
343
-        this.$remoteConnectionMessage.css(
344
-            'left', ((parentWidth/2) - (width/2)));
345 355
     }
346 356
 
347 357
     resize (containerWidth, containerHeight, animate = false) {
@@ -372,7 +382,7 @@ export class VideoContainer extends LargeContainer {
372 382
 
373 383
         this.$avatar.css('top', top);
374 384
 
375
-        this.positionRemoteConnectionMessage();
385
+        this.positionRemoteStatusMessages();
376 386
 
377 387
         this.$wrapper.animate({
378 388
             width: width,

+ 20
- 0
react/features/base/participants/actions.js Vedi File

@@ -204,6 +204,26 @@ export function participantLeft(id) {
204 204
     };
205 205
 }
206 206
 
207
+/**
208
+ * Action to signal that a participant's presence status has changed.
209
+ *
210
+ * @param {string} id - Participant's ID.
211
+ * @param {string} presence - Participant's new presence status.
212
+ * @returns {{
213
+ *     type: PARTICIPANT_UPDATED,
214
+ *     participant: {
215
+ *         id: string,
216
+ *         presence: string
217
+ *     }
218
+ * }}
219
+ */
220
+export function participantPresenceChanged(id, presence) {
221
+    return participantUpdated({
222
+        id,
223
+        presence
224
+    });
225
+}
226
+
207 227
 /**
208 228
  * Action to signal that a participant's role has changed.
209 229
  *

+ 1
- 0
react/features/large-video/components/LargeVideo.web.js Vedi File

@@ -36,6 +36,7 @@ export default class LargeVideo extends Component {
36 36
                         id = 'dominantSpeakerAvatar'
37 37
                         src = '' />
38 38
                 </div>
39
+                <div id = 'remotePresenceMessage' />
39 40
                 <span id = 'remoteConnectionMessage' />
40 41
                 <div>
41 42
                     <div className = 'video_blurred_container'>

+ 81
- 0
react/features/presence-status/components/PresenceLabel.js Vedi File

@@ -0,0 +1,81 @@
1
+import React, { Component } from 'react';
2
+import { connect } from 'react-redux';
3
+
4
+import { getParticipantById } from '../../base/participants';
5
+
6
+/**
7
+ * React {@code Component} for displaying the current presence status of a
8
+ * participant.
9
+ *
10
+ * @extends Component
11
+ */
12
+class PresenceLabel extends Component {
13
+    /**
14
+     * The default values for {@code PresenceLabel} component's property types.
15
+     *
16
+     * @static
17
+     */
18
+    static defaultProps = {
19
+        _presence: ''
20
+    };
21
+
22
+    /**
23
+     * {@code PresenceLabel} component's property types.
24
+     *
25
+     * @static
26
+     */
27
+    static propTypes = {
28
+        /**
29
+         * The current present status associated with the passed in
30
+         * participantID prop.
31
+         */
32
+        _presence: React.PropTypes.string,
33
+
34
+        /**
35
+         * The ID of the participant whose presence status shoul display.
36
+         */
37
+        participantID: React.PropTypes.string
38
+    };
39
+
40
+    /**
41
+     * Implements React's {@link Component#render()}.
42
+     *
43
+     * @inheritdoc
44
+     * @returns {ReactElement}
45
+     */
46
+    render() {
47
+        const { _presence } = this.props;
48
+
49
+        return (
50
+            <div
51
+                className
52
+                    = { `presence-label ${_presence ? '' : 'no-presence'}` }>
53
+                { _presence }
54
+            </div>
55
+        );
56
+    }
57
+}
58
+
59
+/**
60
+ * Maps (parts of) the Redux state to the associated {@code PresenceLabel}'s
61
+ * props.
62
+ *
63
+ * @param {Object} state - The Redux state.
64
+ * @param {Object} ownProps - The React Component props passed to the associated
65
+ * instance of {@code PresenceLabel}.
66
+ * @private
67
+ * @returns {{
68
+ *     _presence: (string|undefined)
69
+ * }}
70
+ */
71
+function _mapStateToProps(state, ownProps) {
72
+    const participant
73
+        = getParticipantById(
74
+            state['features/base/participants'], ownProps.participantID);
75
+
76
+    return {
77
+        _presence: participant && participant.presence
78
+    };
79
+}
80
+
81
+export default connect(_mapStateToProps)(PresenceLabel);

+ 1
- 0
react/features/presence-status/components/index.js Vedi File

@@ -0,0 +1 @@
1
+export { default as PresenceLabel } from './PresenceLabel';

+ 1
- 0
react/features/presence-status/index.js Vedi File

@@ -0,0 +1 @@
1
+export * from './components';

Loading…
Annulla
Salva