Browse Source

Add conference timer (#4958)

master
theunafraid 4 years ago
parent
commit
c2cf09a2ca

+ 9
- 1
conference.js View File

@@ -41,6 +41,7 @@ import {
41 41
     conferenceJoined,
42 42
     conferenceLeft,
43 43
     conferenceSubjectChanged,
44
+    conferenceTimestampChanged,
44 45
     conferenceWillJoin,
45 46
     conferenceWillLeave,
46 47
     dataChannelOpened,
@@ -1818,7 +1819,10 @@ export default {
1818 1819
 
1819 1820
         room.on(
1820 1821
             JitsiConferenceEvents.CONFERENCE_LEFT,
1821
-            (...args) => APP.store.dispatch(conferenceLeft(room, ...args)));
1822
+            (...args) => {
1823
+                APP.store.dispatch(conferenceTimestampChanged(0));
1824
+                APP.store.dispatch(conferenceLeft(room, ...args));
1825
+            });
1822 1826
 
1823 1827
         room.on(
1824 1828
             JitsiConferenceEvents.AUTH_STATUS_CHANGED,
@@ -1948,6 +1952,10 @@ export default {
1948 1952
             JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
1949 1953
             id => APP.store.dispatch(dominantSpeakerChanged(id, room)));
1950 1954
 
1955
+        room.on(
1956
+            JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
1957
+            conferenceTimestamp => APP.store.dispatch(conferenceTimestampChanged(conferenceTimestamp)));
1958
+
1951 1959
         room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
1952 1960
             APP.store.dispatch(localParticipantConnectionStatusChanged(
1953 1961
                 JitsiParticipantConnectionStatus.INTERRUPTED));

+ 6
- 0
css/_subject.scss View File

@@ -23,4 +23,10 @@
23 23
     &-text {
24 24
         vertical-align: middle;
25 25
     }
26
+
27
+    &-conference-timer {
28
+        display: block;
29
+        font-size: 15px;
30
+        opacity: 0.6;
31
+    }
26 32
 }

+ 5
- 0
doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example View File

@@ -30,6 +30,7 @@ VirtualHost "jitmeet.example.com"
30 30
                 certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
31 31
         }
32 32
         speakerstats_component = "speakerstats.jitmeet.example.com"
33
+        conference_duration_component = "conference_duration.jitmeet.example.com"
33 34
         -- we need bosh
34 35
         modules_enabled = {
35 36
             "bosh";
@@ -37,6 +38,7 @@ VirtualHost "jitmeet.example.com"
37 38
             "ping"; -- Enable mod_ping
38 39
             "speakerstats";
39 40
             "turncredentials";
41
+            "conference_duration";
40 42
         }
41 43
         c2s_require_encryption = false
42 44
 
@@ -65,3 +67,6 @@ Component "focus.jitmeet.example.com"
65 67
 
66 68
 Component "speakerstats.jitmeet.example.com" "speakerstats_component"
67 69
     muc_component = "conference.jitmeet.example.com"
70
+
71
+Component "conference_duration.jitmeet.example.com" "conference_duration_component"
72
+    muc_component = "conference.jitmeet.example.com"

+ 2
- 2
package-lock.json View File

@@ -10869,8 +10869,8 @@
10869 10869
       }
10870 10870
     },
10871 10871
     "lib-jitsi-meet": {
10872
-      "version": "github:jitsi/lib-jitsi-meet#9ba4c15e85c6b270c367affab07180a069041ae9",
10873
-      "from": "github:jitsi/lib-jitsi-meet#9ba4c15e85c6b270c367affab07180a069041ae9",
10872
+      "version": "github:jitsi/lib-jitsi-meet#f35c74222f644b6420cddd8f6a4a2dfffe451f3b",
10873
+      "from": "github:jitsi/lib-jitsi-meet#f35c74222f644b6420cddd8f6a4a2dfffe451f3b",
10874 10874
       "requires": {
10875 10875
         "@jitsi/sdp-interop": "0.1.14",
10876 10876
         "@jitsi/sdp-simulcast": "0.2.2",

+ 1
- 1
package.json View File

@@ -56,7 +56,7 @@
56 56
     "js-utils": "github:jitsi/js-utils#400ce825d3565019946ee75d86ed773c6f21e117",
57 57
     "jsrsasign": "8.0.12",
58 58
     "jwt-decode": "2.2.0",
59
-    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#9ba4c15e85c6b270c367affab07180a069041ae9",
59
+    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#f35c74222f644b6420cddd8f6a4a2dfffe451f3b",
60 60
     "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
61 61
     "lodash": "4.17.13",
62 62
     "moment": "2.19.4",

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

@@ -52,6 +52,16 @@ export const CONFERENCE_LEFT = 'CONFERENCE_LEFT';
52 52
  */
53 53
 export const CONFERENCE_SUBJECT_CHANGED = 'CONFERENCE_SUBJECT_CHANGED';
54 54
 
55
+/**
56
+* The type of (redux) action, which indicates conference UTC timestamp changes.
57
+*
58
+* {
59
+*      type: CONFERENCE_TIMESTAMP_CHANGED
60
+*      timestamp: number
61
+* }
62
+*/
63
+export const CONFERENCE_TIMESTAMP_CHANGED = 'CONFERENCE_TIMESTAMP_CHANGED';
64
+
55 65
 /**
56 66
  * The type of (redux) action which signals that a specific conference will be
57 67
  * joined.

+ 24
- 1
react/features/base/conference/actions.js View File

@@ -35,6 +35,7 @@ import {
35 35
     CONFERENCE_JOINED,
36 36
     CONFERENCE_LEFT,
37 37
     CONFERENCE_SUBJECT_CHANGED,
38
+    CONFERENCE_TIMESTAMP_CHANGED,
38 39
     CONFERENCE_WILL_JOIN,
39 40
     CONFERENCE_WILL_LEAVE,
40 41
     DATA_CHANNEL_OPENED,
@@ -93,10 +94,16 @@ function _addConferenceListeners(conference, dispatch) {
93 94
         (...args) => dispatch(conferenceJoined(conference, ...args)));
94 95
     conference.on(
95 96
         JitsiConferenceEvents.CONFERENCE_LEFT,
96
-        (...args) => dispatch(conferenceLeft(conference, ...args)));
97
+        (...args) => {
98
+            dispatch(conferenceTimestampChanged(0));
99
+            dispatch(conferenceLeft(conference, ...args));
100
+        });
97 101
     conference.on(JitsiConferenceEvents.SUBJECT_CHANGED,
98 102
         (...args) => dispatch(conferenceSubjectChanged(...args)));
99 103
 
104
+    conference.on(JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
105
+        (...args) => dispatch(conferenceTimestampChanged(...args)));
106
+
100 107
     conference.on(
101 108
         JitsiConferenceEvents.KICKED,
102 109
         (...args) => dispatch(kickedOut(conference, ...args)));
@@ -313,6 +320,22 @@ export function conferenceSubjectChanged(subject: string) {
313 320
     };
314 321
 }
315 322
 
323
+/**
324
+* Signals that the conference timestamp has been changed.
325
+*
326
+* @param {number} conferenceTimestamp - The UTC timestamp.
327
+* @returns {{
328
+*       type: CONFERENCE_TIMESTAMP_CHANGED,
329
+*       conferenceTimestamp
330
+* }}
331
+*/
332
+export function conferenceTimestampChanged(conferenceTimestamp: number) {
333
+    return {
334
+        type: CONFERENCE_TIMESTAMP_CHANGED,
335
+        conferenceTimestamp
336
+    };
337
+}
338
+
316 339
 /**
317 340
  * Adds any existing local tracks to a specific conference before the conference
318 341
  * is joined. Then signals the intention of the application to have the local

+ 14
- 0
react/features/base/conference/functions.js View File

@@ -167,6 +167,20 @@ export function getConferenceName(stateful: Function | Object): string {
167 167
         || _.startCase(safeDecodeURIComponent(room));
168 168
 }
169 169
 
170
+/**
171
+* Returns the UTC timestamp when the first participant joined the conference.
172
+*
173
+* @param {Function | Object} stateful - Reference that can be resolved to Redux
174
+* state with the {@code toState} function.
175
+* @returns {number}
176
+*/
177
+export function getConferenceTimestamp(stateful: Function | Object): number {
178
+    const state = toState(stateful);
179
+    const { conferenceTimestamp } = state['features/base/conference'];
180
+
181
+    return conferenceTimestamp;
182
+}
183
+
170 184
 /**
171 185
  * Returns the current {@code JitsiConference} which is joining or joined and is
172 186
  * not leaving. Please note the contrast with merely reading the

+ 4
- 0
react/features/base/conference/reducer.js View File

@@ -11,6 +11,7 @@ import {
11 11
     CONFERENCE_JOINED,
12 12
     CONFERENCE_LEFT,
13 13
     CONFERENCE_SUBJECT_CHANGED,
14
+    CONFERENCE_TIMESTAMP_CHANGED,
14 15
     CONFERENCE_WILL_JOIN,
15 16
     CONFERENCE_WILL_LEAVE,
16 17
     LOCK_STATE_CHANGED,
@@ -59,6 +60,9 @@ ReducerRegistry.register(
59 60
         case CONFERENCE_SUBJECT_CHANGED:
60 61
             return set(state, 'subject', action.subject);
61 62
 
63
+        case CONFERENCE_TIMESTAMP_CHANGED:
64
+            return set(state, 'conferenceTimestamp', action.conferenceTimestamp);
65
+
62 66
         case CONFERENCE_LEFT:
63 67
         case CONFERENCE_WILL_LEAVE:
64 68
             return _conferenceLeftOrWillLeave(state, action);

+ 176
- 0
react/features/conference/components/ConferenceTimer.js View File

@@ -0,0 +1,176 @@
1
+// @flow
2
+
3
+import { Component } from 'react';
4
+
5
+import { connect } from '../../base/redux';
6
+import { getLocalizedDurationFormatter } from '../../base/i18n';
7
+import { getConferenceTimestamp } from '../../base/conference/functions';
8
+import { renderConferenceTimer } from '../';
9
+
10
+/**
11
+ * The type of the React {@code Component} props of {@link ConferenceTimer}.
12
+ */
13
+type Props = {
14
+
15
+    /**
16
+     * The UTC timestamp representing the time when first participant joined.
17
+     */
18
+    _startTimestamp: ?number,
19
+
20
+    /**
21
+     * The redux {@code dispatch} function.
22
+     */
23
+    dispatch: Function
24
+};
25
+
26
+/**
27
+ * The type of the React {@code Component} state of {@link ConferenceTimer}.
28
+ */
29
+type State = {
30
+
31
+    /**
32
+     * Value of current conference time.
33
+     */
34
+    timerValue: string
35
+};
36
+
37
+/**
38
+ * ConferenceTimer react component.
39
+ *
40
+ * @class ConferenceTimer
41
+ * @extends Component
42
+ */
43
+class ConferenceTimer extends Component<Props, State> {
44
+
45
+    /**
46
+     * Handle for setInterval timer.
47
+     */
48
+    _interval;
49
+
50
+    /**
51
+     * Initializes a new {@code ConferenceTimer} instance.
52
+     *
53
+     * @param {Props} props - The read-only properties with which the new
54
+     * instance is to be initialized.
55
+     */
56
+    constructor(props: Props) {
57
+        super(props);
58
+
59
+        this.state = {
60
+            timerValue: getLocalizedDurationFormatter(0)
61
+        };
62
+    }
63
+
64
+    /**
65
+     * Starts the conference timer when component will be
66
+     * mounted.
67
+     *
68
+     * @inheritdoc
69
+     */
70
+    componentDidMount() {
71
+        this._startTimer();
72
+    }
73
+
74
+    /**
75
+     * Stops the conference timer when component will be
76
+     * unmounted.
77
+     *
78
+     * @inheritdoc
79
+     */
80
+    componentWillUnmount() {
81
+        this._stopTimer();
82
+    }
83
+
84
+    /**
85
+     * Implements React's {@link Component#render()}.
86
+     *
87
+     * @inheritdoc
88
+     * @returns {ReactElement}
89
+     */
90
+    render() {
91
+        const { timerValue } = this.state;
92
+        const { _startTimestamp } = this.props;
93
+
94
+        if (!_startTimestamp) {
95
+            return null;
96
+        }
97
+
98
+        return renderConferenceTimer(timerValue);
99
+    }
100
+
101
+    /**
102
+     * Sets the current state values that will be used to render the timer.
103
+     *
104
+     * @param {number} refValueUTC - The initial UTC timestamp value.
105
+     * @param {number} currentValueUTC - The current UTC timestamp value.
106
+     *
107
+     * @returns {void}
108
+     */
109
+    _setStateFromUTC(refValueUTC, currentValueUTC) {
110
+
111
+        if (!refValueUTC || !currentValueUTC) {
112
+            return;
113
+        }
114
+
115
+        if (currentValueUTC < refValueUTC) {
116
+            return;
117
+        }
118
+
119
+        const timerMsValue = currentValueUTC - refValueUTC;
120
+
121
+        const localizedTime = getLocalizedDurationFormatter(timerMsValue);
122
+
123
+        this.setState({
124
+            timerValue: localizedTime
125
+        });
126
+    }
127
+
128
+    /**
129
+     * Start conference timer.
130
+     *
131
+     * @returns {void}
132
+     */
133
+    _startTimer() {
134
+        if (!this._interval) {
135
+            this._setStateFromUTC(this.props._startTimestamp, (new Date()).getTime());
136
+
137
+            this._interval = setInterval(() => {
138
+                this._setStateFromUTC(this.props._startTimestamp, (new Date()).getTime());
139
+            }, 1000);
140
+        }
141
+    }
142
+
143
+    /**
144
+     * Stop conference timer.
145
+     *
146
+     * @returns {void}
147
+     */
148
+    _stopTimer() {
149
+        if (this._interval) {
150
+            clearInterval(this._interval);
151
+        }
152
+
153
+        this.setState({
154
+            timerValue: getLocalizedDurationFormatter(0)
155
+        });
156
+    }
157
+}
158
+
159
+/**
160
+ * Maps (parts of) the Redux state to the associated
161
+ * {@code ConferenceTimer}'s props.
162
+ *
163
+ * @param {Object} state - The Redux state.
164
+ * @private
165
+ * @returns {{
166
+ *      _startTimestamp: number
167
+ * }}
168
+ */
169
+export function _mapStateToProps(state: Object) {
170
+
171
+    return {
172
+        _startTimestamp: getConferenceTimestamp(state)
173
+    };
174
+}
175
+
176
+export default connect(_mapStateToProps)(ConferenceTimer);

+ 23
- 0
react/features/conference/components/native/ConferenceTimerDisplay.js View File

@@ -0,0 +1,23 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { Text } from 'react-native';
5
+
6
+import styles from './styles';
7
+
8
+/**
9
+ * Returns native element to be rendered.
10
+ *
11
+ * @param {string} timerValue - String to display as time.
12
+ *
13
+ * @returns {ReactElement}
14
+ */
15
+export default function renderConferenceTimer(timerValue: string) {
16
+    return (
17
+        <Text
18
+            numberOfLines = { 4 }
19
+            style = { styles.roomTimer }>
20
+            { timerValue }
21
+        </Text>
22
+    );
23
+}

+ 2
- 0
react/features/conference/components/native/NavigationBar.js View File

@@ -9,6 +9,7 @@ import { connect } from '../../../base/redux';
9 9
 import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
10 10
 import { isToolboxVisible } from '../../../toolbox';
11 11
 
12
+import ConferenceTimer from '../ConferenceTimer';
12 13
 import styles, { NAVBAR_GRADIENT_COLORS } from './styles';
13 14
 
14 15
 type Props = {
@@ -63,6 +64,7 @@ class NavigationBar extends Component<Props> {
63 64
                         style = { styles.roomName }>
64 65
                         { this.props._meetingName }
65 66
                     </Text>
67
+                    <ConferenceTimer />
66 68
                 </View>
67 69
             </View>
68 70
         ];

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

@@ -1,3 +1,4 @@
1 1
 // @flow
2 2
 
3 3
 export { default as Conference } from './Conference';
4
+export { default as renderConferenceTimer } from './ConferenceTimerDisplay';

+ 8
- 2
react/features/conference/components/native/styles.js View File

@@ -105,6 +105,12 @@ export default {
105 105
         paddingHorizontal: 14
106 106
     },
107 107
 
108
+    roomTimer: {
109
+        color: ColorPalette.white,
110
+        fontSize: 15,
111
+        opacity: 0.6
112
+    },
113
+
108 114
     roomName: {
109 115
         color: ColorPalette.white,
110 116
         fontSize: 17,
@@ -112,8 +118,8 @@ export default {
112 118
     },
113 119
 
114 120
     roomNameWrapper: {
115
-        flexDirection: 'row',
116
-        justifyContent: 'center',
121
+        flexDirection: 'column',
122
+        alignItems: 'center',
117 123
         left: 0,
118 124
         paddingHorizontal: 48,
119 125
         position: 'absolute',

+ 16
- 0
react/features/conference/components/web/ConferenceTimerDisplay.js View File

@@ -0,0 +1,16 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+/**
6
+ * Returns web element to be rendered.
7
+ *
8
+ * @param {string} timerValue - String to display as time.
9
+ *
10
+ * @returns {ReactElement}
11
+ */
12
+export default function renderConferenceTimer(timerValue: string) {
13
+    return (
14
+        <span className = 'subject-conference-timer' >{ timerValue }</span>
15
+    );
16
+}

+ 2
- 0
react/features/conference/components/web/Subject.js View File

@@ -7,6 +7,7 @@ import { getParticipantCount } from '../../../base/participants/functions';
7 7
 import { connect } from '../../../base/redux';
8 8
 import { isToolboxVisible } from '../../../toolbox';
9 9
 
10
+import ConferenceTimer from '../ConferenceTimer';
10 11
 import ParticipantsCount from './ParticipantsCount';
11 12
 
12 13
 /**
@@ -51,6 +52,7 @@ class Subject extends Component<Props> {
51 52
             <div className = { `subject ${_visible ? 'visible' : ''}` }>
52 53
                 <span className = 'subject-text'>{ _subject }</span>
53 54
                 { _showParticipantCount && <ParticipantsCount /> }
55
+                <ConferenceTimer />
54 56
             </div>
55 57
         );
56 58
     }

+ 2
- 0
react/features/conference/components/web/index.js View File

@@ -1,3 +1,5 @@
1 1
 // @flow
2 2
 
3 3
 export { default as Conference } from './Conference';
4
+export { default as renderConferenceTimer } from './ConferenceTimerDisplay';
5
+

+ 5
- 0
resources/prosody-plugins/mod_conference_duration.lua View File

@@ -0,0 +1,5 @@
1
+local conference_duration_component
2
+    = module:get_option_string(
3
+        "conference_duration_component", "conference_duration"..module.host);
4
+
5
+module:add_identity("component", "conference_duration", conference_duration_component);

+ 66
- 0
resources/prosody-plugins/mod_conference_duration_component.lua View File

@@ -0,0 +1,66 @@
1
+local st = require "util.stanza";
2
+local socket = require "socket";
3
+local json = require "util.json";
4
+local ext_events = module:require "ext_events";
5
+local it = require "util.iterators";
6
+
7
+-- we use async to detect Prosody 0.10 and earlier
8
+local have_async = pcall(require, "util.async");
9
+if not have_async then
10
+    module:log("warn", "conference duration will not work with Prosody version 0.10 or less.");
11
+    return;
12
+end
13
+
14
+local muc_component_host = module:get_option_string("muc_component");
15
+if muc_component_host == nil then
16
+    log("error", "No muc_component specified. No muc to operate on!");
17
+    return;
18
+end
19
+
20
+log("info", "Starting conference duration timer for %s", muc_component_host);
21
+
22
+function occupant_joined(event)
23
+    local room = event.room;
24
+    local occupant = event.occupant;
25
+
26
+    local participant_count = it.count(room:each_occupant());
27
+
28
+    if participant_count > 1 then
29
+
30
+        if room.created_timestamp == nil then
31
+            room.created_timestamp = os.time(os.date("!*t")) * 1000; -- Lua provides UTC time in seconds, so convert to milliseconds
32
+        end
33
+
34
+        local body_json = {};
35
+        body_json.type = 'conference_duration';
36
+        body_json.created_timestamp = room.created_timestamp;
37
+
38
+        local stanza = st.message({
39
+            from = module.host;
40
+            to = occupant.jid;
41
+        })
42
+        :tag("json-message", {xmlns='http://jitsi.org/jitmeet'})
43
+        :text(json.encode(body_json)):up();
44
+
45
+        room:route_stanza(stanza);
46
+    end
47
+end
48
+
49
+-- executed on every host added internally in prosody, including components
50
+function process_host(host)
51
+    if host == muc_component_host then -- the conference muc component
52
+        module:log("info", "Hook to muc events on %s", host);
53
+
54
+       local muc_module = module:context(host)
55
+       muc_module:hook("muc-occupant-joined", occupant_joined, -1);
56
+    end
57
+end
58
+
59
+if prosody.hosts[muc_component_host] == nil then
60
+    module:log("info", "No muc component found, will listen for it: %s", muc_component_host);
61
+
62
+    -- when a host or component is added
63
+    prosody.events.add_handler("host-activated", process_host);
64
+else
65
+    process_host(muc_component_host);
66
+end

Loading…
Cancel
Save