Quellcode durchsuchen

Modal dialog for displaying dominant speaker times

j8
Leonard Kim vor 8 Jahren
Ursprung
Commit
989161159d

+ 12
- 0
conference.js Datei anzeigen

@@ -714,6 +714,18 @@ export default {
714 714
     sendFeedback (overallFeedback, detailedFeedback) {
715 715
         return room.sendFeedback (overallFeedback, detailedFeedback);
716 716
     },
717
+
718
+    /**
719
+     * Get speaker stats that track total dominant speaker time.
720
+     *
721
+     * @returns {object} A hash with keys being user ids and values being the
722
+     * library's SpeakerStats model used for calculating time as dominant
723
+     * speaker.
724
+     */
725
+    getSpeakerStats() {
726
+        return room.getSpeakerStats();
727
+    },
728
+
717 729
     /**
718 730
      * Returns the connection times stored in the library.
719 731
      */

+ 1
- 0
css/main.scss Datei anzeigen

@@ -39,6 +39,7 @@
39 39
 @import 'reload_overlay/reload_overlay';
40 40
 @import 'modals/dialog';
41 41
 @import 'modals/feedback/feedback';
42
+@import 'modals/speaker_stats/speaker_stats';
42 43
 @import 'videolayout_default';
43 44
 @import 'notice';
44 45
 @import 'popup_menu';

+ 56
- 0
css/modals/speaker_stats/_speaker_stats.scss Datei anzeigen

@@ -0,0 +1,56 @@
1
+.speaker-stats {
2
+    list-style: none;
3
+    padding: 0;
4
+    color: $auiDialogColor;
5
+    width: 100%;
6
+    font-weight: 500;
7
+
8
+    .speaker-stats-item__status-dot {
9
+        position: relative;
10
+        display: block;
11
+        width: 9px;
12
+        height: 9px;
13
+        border-radius: 50%;
14
+        margin: 0 auto;
15
+
16
+        &.status-active {
17
+            background: green;
18
+        }
19
+
20
+        &.status-inactive {
21
+            background: gray;
22
+        }
23
+    }
24
+
25
+    .status-user-left {
26
+        color: $placeHolderColor;
27
+    }
28
+
29
+    .speaker-stats-item__status,
30
+    .speaker-stats-item__name,
31
+    .speaker-stats-item__time {
32
+        display: inline-block;
33
+        margin: 5px 0;
34
+        vertical-align: middle;
35
+    }
36
+    .speaker-stats-item__status {
37
+        width: 5%;
38
+    }
39
+    .speaker-stats-item__name {
40
+        width: 40%;
41
+    }
42
+    .speaker-stats-item__time {
43
+        width: 55%;
44
+    }
45
+
46
+    .speaker-stats-item:nth-child(even) {
47
+        background: whitesmoke;
48
+    }
49
+
50
+    .speaker-stats-item__name,
51
+    .speaker-stats-item__time {
52
+        overflow: hidden;
53
+        text-overflow: ellipsis;
54
+        white-space: nowrap;
55
+    }
56
+}

+ 13
- 2
lang/main.json Datei anzeigen

@@ -36,7 +36,8 @@
36 36
         "toggleChat": "Open or close the chat",
37 37
         "mute": "Mute or unmute your microphone",
38 38
         "fullScreen": "Enter or exit full screen",
39
-        "videoMute": "Start or stop your camera"
39
+        "videoMute": "Start or stop your camera",
40
+        "showSpeakerStats": "Show speaker stats"
40 41
     },
41 42
     "welcomepage":{
42 43
         "disable": "Don't show this page again",
@@ -335,7 +336,8 @@
335 336
         "remoteControlDeniedMessage": "__user__ rejected your remote control request!",
336 337
         "remoteControlAllowedMessage": "__user__ accepted your remote control request!",
337 338
         "remoteControlErrorMessage": "An error occurred while trying to request remote control permissions from __user__!",
338
-        "remoteControlStopMessage": "The remote control session ended!"
339
+        "remoteControlStopMessage": "The remote control session ended!",
340
+        "close": "Close"
339 341
     },
340 342
     "email":
341 343
     {
@@ -404,5 +406,14 @@
404 406
         "streamIdHelp": "Where do I find this?",
405 407
         "error": "Live streaming failed. Please try again.",
406 408
         "busy": "All recorders are currently busy. Please try again later."
409
+    },
410
+    "speakerStats":
411
+    {
412
+        "hours": "__count__h",
413
+        "minutes": "__count__m",
414
+        "name": "Name",
415
+        "seconds": "__count__s",
416
+        "speakerStats": "Speaker Stats",
417
+        "speakerTime": "Speaker Time"
407 418
     }
408 419
 }

+ 11
- 0
modules/keyboardshortcut/keyboardshortcut.js Datei anzeigen

@@ -1,5 +1,10 @@
1 1
 /* global APP, $, JitsiMeetJS */
2 2
 
3
+import {
4
+    toggleDialog
5
+} from '../../react/features/base/dialog';
6
+import { SpeakerStats } from '../../react/features/speaker-stats';
7
+
3 8
 /**
4 9
  * The reference to the shortcut dialogs when opened.
5 10
  */
@@ -29,6 +34,12 @@ function initGlobalShortcuts() {
29 34
     });
30 35
     KeyboardShortcut._addShortcutToHelp("SPACE","keyboardShortcuts.pushToTalk");
31 36
 
37
+    KeyboardShortcut.registerShortcut("T", null, () => {
38
+        APP.store.dispatch(toggleDialog(SpeakerStats, {
39
+            conference: APP.conference
40
+        }));
41
+    }, "keyboardShortcuts.showSpeakerStats");
42
+
32 43
     /**
33 44
      * FIXME: Currently focus keys are directly implemented below in onkeyup.
34 45
      * They should be moved to the SmallVideo instead.

+ 22
- 0
react/features/base/dialog/actions.js Datei anzeigen

@@ -30,3 +30,25 @@ export function openDialog(component, componentProps) {
30 30
         componentProps
31 31
     };
32 32
 }
33
+
34
+/**
35
+ * Signals Dialog to open a dialog with the specified component if the component
36
+ * is not already open. If it is open, then Dialog is signaled to close
37
+ * its dialog.
38
+ *
39
+ * @param {Object} component - The component to display as dialog.
40
+ * @param {Object} componentProps - The properties needed for that component.
41
+ * @returns {Object}
42
+ */
43
+export function toggleDialog(component, componentProps) {
44
+    return (dispatch, getState) => {
45
+        const state = getState();
46
+        const dialogState = state['features/base/dialog'];
47
+
48
+        if (dialogState.component === component) {
49
+            dispatch(hideDialog());
50
+        } else {
51
+            dispatch(openDialog(component, componentProps));
52
+        }
53
+    };
54
+}

+ 29
- 0
react/features/base/util/timeUtils.js Datei anzeigen

@@ -0,0 +1,29 @@
1
+/**
2
+ * Counts how many whole hours are included in the given time total.
3
+ *
4
+ * @param {number} milliseconds - The millisecond total to get hours from.
5
+ * @returns {number}
6
+ */
7
+export function getHoursCount(milliseconds) {
8
+    return Math.floor(milliseconds / (60 * 60 * 1000));
9
+}
10
+
11
+/**
12
+ * Counts how many whole minutes are included in the given time total.
13
+ *
14
+ * @param {number} milliseconds - The millisecond total to get minutes from.
15
+ * @returns {number}
16
+ */
17
+export function getMinutesCount(milliseconds) {
18
+    return Math.floor(milliseconds / (60 * 1000) % 60);
19
+}
20
+
21
+/**
22
+ * Counts how many whole seconds are included in the given time total.
23
+ *
24
+ * @param {number} milliseconds - The millisecond total to get seconds from.
25
+ * @returns {number}
26
+ */
27
+export function getSecondsCount(milliseconds) {
28
+    return Math.floor(milliseconds / 1000 % 60);
29
+}

+ 150
- 0
react/features/speaker-stats/components/SpeakerStats.js Datei anzeigen

@@ -0,0 +1,150 @@
1
+/* global APP, interfaceConfig */
2
+
3
+import React, { Component } from 'react';
4
+
5
+import { Dialog } from '../../base/dialog';
6
+import { translate } from '../../base/i18n';
7
+import SpeakerStatsItem from './SpeakerStatsItem';
8
+import SpeakerStatsLabels from './SpeakerStatsLabels';
9
+
10
+/**
11
+ * React component for displaying a list of speaker stats.
12
+ *
13
+ * @extends Component
14
+ */
15
+class SpeakerStats extends Component {
16
+    /**
17
+     * SpeakerStats component's property types.
18
+     *
19
+     * @static
20
+     */
21
+    static propTypes = {
22
+        /**
23
+         * The JitsiConference from which stats will be pulled.
24
+         */
25
+        conference: React.PropTypes.object,
26
+
27
+        /**
28
+         * The function to translate human-readable text.
29
+         */
30
+        t: React.PropTypes.func
31
+    }
32
+
33
+    /**
34
+     * Initializes a new SpeakerStats instance.
35
+     *
36
+     * @param {Object} props - The read-only React Component props with which
37
+     * the new instance is to be initialized.
38
+     */
39
+    constructor(props) {
40
+        super(props);
41
+
42
+        this.state = {
43
+            stats: {}
44
+        };
45
+        this._updateInterval = null;
46
+        this._updateStats = this._updateStats.bind(this);
47
+    }
48
+
49
+    /**
50
+     * Immediately request for updated speaker stats and begin
51
+     * polling for speaker stats updates.
52
+     *
53
+     * @inheritdoc
54
+     * @returns {void}
55
+     */
56
+    componentWillMount() {
57
+        this._updateStats();
58
+        this._updateInterval = setInterval(this._updateStats, 1000);
59
+    }
60
+
61
+    /**
62
+     * Stop polling for speaker stats updates.
63
+     *
64
+     * @inheritdoc
65
+     * @returns {void}
66
+     */
67
+    componentWillUnmount() {
68
+        clearInterval(this._updateInterval);
69
+    }
70
+
71
+    /**
72
+     * Implements React's {@link Component#render()}.
73
+     *
74
+     * @inheritdoc
75
+     * @returns {ReactElement}
76
+     */
77
+    render() {
78
+        const userIds = Object.keys(this.state.stats);
79
+        const items = userIds.map(userId => this._createStatsItem(userId));
80
+
81
+        return (
82
+            <Dialog
83
+                cancelTitleKey = { 'dialog.close' }
84
+                submitDisabled = { true }
85
+                titleKey = 'speakerStats.speakerStats'>
86
+                <div className = 'speaker-stats'>
87
+                    <SpeakerStatsLabels />
88
+                    { items }
89
+                </div>
90
+            </Dialog>
91
+        );
92
+    }
93
+
94
+   /**
95
+     * Update the internal state with the latest speaker stats.
96
+     *
97
+     * @returns {void}
98
+     * @private
99
+     */
100
+    _updateStats() {
101
+        const stats = this.props.conference.getSpeakerStats();
102
+
103
+        this.setState({ stats });
104
+    }
105
+
106
+    /**
107
+     * Create a SpeakerStatsItem instance for the passed in user id.
108
+     *
109
+     * @param {string} userId -  User id used to look up the associated
110
+     * speaker stats from the jitsi library.
111
+     * @returns {SpeakerStatsItem|null}
112
+     * @private
113
+     */
114
+    _createStatsItem(userId) {
115
+        const statsModel = this.state.stats[userId];
116
+
117
+        if (!statsModel) {
118
+            return null;
119
+        }
120
+
121
+        const isDominantSpeaker = statsModel.isDominantSpeaker();
122
+        const dominantSpeakerTime = statsModel.getTotalDominantSpeakerTime();
123
+        const hasLeft = statsModel.hasLeft();
124
+
125
+        let displayName = '';
126
+
127
+        if (statsModel.isLocalStats()) {
128
+            const { t } = this.props;
129
+            const meString = t('me');
130
+
131
+            displayName = APP.settings.getDisplayName();
132
+            displayName = displayName ? `${displayName} (${meString})`
133
+                : meString;
134
+        } else {
135
+            displayName = this.state.stats[userId].getDisplayName()
136
+                || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
137
+        }
138
+
139
+        return (
140
+            <SpeakerStatsItem
141
+                displayName = { displayName }
142
+                dominantSpeakerTime = { dominantSpeakerTime }
143
+                hasLeft = { hasLeft }
144
+                isDominantSpeaker = { isDominantSpeaker }
145
+                key = { userId } />
146
+        );
147
+    }
148
+}
149
+
150
+export default translate(SpeakerStats);

+ 69
- 0
react/features/speaker-stats/components/SpeakerStatsItem.js Datei anzeigen

@@ -0,0 +1,69 @@
1
+import React, { Component } from 'react';
2
+
3
+import TimeElapsed from './TimeElapsed';
4
+
5
+/**
6
+ * React component for display an individual user's speaker stats.
7
+ *
8
+ * @extends Component
9
+ */
10
+class SpeakerStatsItem extends Component {
11
+    /**
12
+     * SpeakerStatsItem component's property types.
13
+     *
14
+     * @static
15
+     */
16
+    static propTypes = {
17
+        /**
18
+         * The name of the participant.
19
+         */
20
+        displayName: React.PropTypes.string,
21
+
22
+        /**
23
+         * The total milliseconds the participant has been dominant speaker.
24
+         */
25
+        dominantSpeakerTime: React.PropTypes.number,
26
+
27
+        /**
28
+         * True if the participant is no longer in the meeting.
29
+         */
30
+        hasLeft: React.PropTypes.bool,
31
+
32
+        /**
33
+         * True if the participant is currently the dominant speaker.
34
+         */
35
+        isDominantSpeaker: React.PropTypes.bool
36
+    }
37
+
38
+    /**
39
+     * Implements React's {@link Component#render()}.
40
+     *
41
+     * @inheritdoc
42
+     * @returns {ReactElement}
43
+     */
44
+    render() {
45
+        const hasLeftClass = this.props.hasLeft ? 'status-user-left' : '';
46
+        const rowDisplayClass = `speaker-stats-item ${hasLeftClass}`;
47
+
48
+        const dotClass = this.props.isDominantSpeaker
49
+            ? 'status-active' : 'status-inactive';
50
+        const speakerStatusClass = `speaker-stats-item__status-dot ${dotClass}`;
51
+
52
+        return (
53
+            <div className = { rowDisplayClass }>
54
+                <div className = 'speaker-stats-item__status'>
55
+                    <span className = { speakerStatusClass } />
56
+                </div>
57
+                <div className = 'speaker-stats-item__name'>
58
+                    { this.props.displayName }
59
+                </div>
60
+                <div className = 'speaker-stats-item__time'>
61
+                    <TimeElapsed
62
+                        time = { this.props.dominantSpeakerTime } />
63
+                </div>
64
+            </div>
65
+        );
66
+    }
67
+}
68
+
69
+export default SpeakerStatsItem;

+ 46
- 0
react/features/speaker-stats/components/SpeakerStatsLabels.js Datei anzeigen

@@ -0,0 +1,46 @@
1
+import React, { Component } from 'react';
2
+
3
+import { translate } from '../../base/i18n';
4
+
5
+/**
6
+ * React component for labeling speaker stats column items.
7
+ *
8
+ * @extends Component
9
+ */
10
+class SpeakerStatsLabels extends Component {
11
+    /**
12
+     * SpeakerStatsLabels component's property types.
13
+     *
14
+     * @static
15
+     */
16
+    static propTypes = {
17
+        /**
18
+         * The function to translate human-readable text.
19
+         */
20
+        t: React.PropTypes.func
21
+    }
22
+
23
+    /**
24
+     * Implements React's {@link Component#render()}.
25
+     *
26
+     * @inheritdoc
27
+     * @returns {ReactElement}
28
+     */
29
+    render() {
30
+        const { t } = this.props;
31
+
32
+        return (
33
+            <div className = 'speaker-stats-item__labels'>
34
+                <div className = 'speaker-stats-item__status' />
35
+                <div className = 'speaker-stats-item__name'>
36
+                    { t('speakerStats.name') }
37
+                </div>
38
+                <div className = 'speaker-stats-item__time'>
39
+                    { t('speakerStats.speakerTime') }
40
+                </div>
41
+            </div>
42
+        );
43
+    }
44
+}
45
+
46
+export default translate(SpeakerStatsLabels);

+ 94
- 0
react/features/speaker-stats/components/TimeElapsed.js Datei anzeigen

@@ -0,0 +1,94 @@
1
+import React, { Component } from 'react';
2
+
3
+import { translate } from '../../base/i18n';
4
+import {
5
+    getHoursCount,
6
+    getMinutesCount,
7
+    getSecondsCount
8
+} from '../../base/util/timeUtils';
9
+
10
+/**
11
+ * React component for displaying total time elapsed. Converts a total count of
12
+ * milliseconds into a more humanized form: "# hours, # minutes, # seconds".
13
+ * With a time of 0, "0s" will be displayed.
14
+ *
15
+ * @extends Component
16
+ */
17
+class TimeElapsed extends Component {
18
+    /**
19
+     * TimeElapsed component's property types.
20
+     *
21
+     * @static
22
+     */
23
+    static propTypes = {
24
+        /**
25
+         * The function to translate human-readable text.
26
+         */
27
+        t: React.PropTypes.func,
28
+
29
+        /**
30
+         * The milliseconds to be converted into a humanized format.
31
+         */
32
+        time: React.PropTypes.number
33
+    }
34
+
35
+    /**
36
+     * Implements React's {@link Component#render()}.
37
+     *
38
+     * @inheritdoc
39
+     * @returns {ReactElement}
40
+     */
41
+    render() {
42
+        const hours = getHoursCount(this.props.time);
43
+        const minutes = getMinutesCount(this.props.time);
44
+        const seconds = getSecondsCount(this.props.time);
45
+        const timeElapsed = [];
46
+
47
+        if (hours) {
48
+            const hourPassed = this._createTimeDisplay(hours,
49
+                'speakerStats.hours', 'hours');
50
+
51
+            timeElapsed.push(hourPassed);
52
+        }
53
+
54
+        if (hours || minutes) {
55
+            const minutesPassed = this._createTimeDisplay(minutes,
56
+                'speakerStats.minutes', 'minutes');
57
+
58
+            timeElapsed.push(minutesPassed);
59
+        }
60
+
61
+        const secondsPassed = this._createTimeDisplay(seconds,
62
+            'speakerStats.seconds', 'seconds');
63
+
64
+        timeElapsed.push(secondsPassed);
65
+
66
+        return (
67
+            <div>
68
+                { timeElapsed }
69
+            </div>
70
+        );
71
+    }
72
+
73
+    /**
74
+     * Returns a ReactElement to display the passed in count and a count noun.
75
+     *
76
+     * @private
77
+     * @param {number} count - The number used for display and to check for
78
+     * count noun plurality.
79
+     * @param {string} countNounKey - Translation key for the time's count noun.
80
+     * @param {string} countType - What is being counted. Used as the element's
81
+     * key for react to iterate upon.
82
+     * @returns {ReactElement}
83
+     */
84
+    _createTimeDisplay(count, countNounKey, countType) {
85
+        const { t } = this.props;
86
+
87
+        return (
88
+            <span key = { countType } > { t(countNounKey, { count }) } </span>
89
+        );
90
+    }
91
+
92
+}
93
+
94
+export default translate(TimeElapsed);

+ 1
- 0
react/features/speaker-stats/components/index.js Datei anzeigen

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

+ 1
- 0
react/features/speaker-stats/index.js Datei anzeigen

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

Laden…
Abbrechen
Speichern