Add conference timer (#4958)

pull/4987/head jitsi-meet_4165
theunafraid 5 years ago committed by Дамян Минков
parent c73ba37202
commit c2cf09a2ca
  1. 10
      conference.js
  2. 6
      css/_subject.scss
  3. 5
      doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example
  4. 4
      package-lock.json
  5. 2
      package.json
  6. 10
      react/features/base/conference/actionTypes.js
  7. 25
      react/features/base/conference/actions.js
  8. 14
      react/features/base/conference/functions.js
  9. 4
      react/features/base/conference/reducer.js
  10. 176
      react/features/conference/components/ConferenceTimer.js
  11. 23
      react/features/conference/components/native/ConferenceTimerDisplay.js
  12. 2
      react/features/conference/components/native/NavigationBar.js
  13. 1
      react/features/conference/components/native/index.js
  14. 10
      react/features/conference/components/native/styles.js
  15. 16
      react/features/conference/components/web/ConferenceTimerDisplay.js
  16. 2
      react/features/conference/components/web/Subject.js
  17. 2
      react/features/conference/components/web/index.js
  18. 5
      resources/prosody-plugins/mod_conference_duration.lua
  19. 66
      resources/prosody-plugins/mod_conference_duration_component.lua

@ -41,6 +41,7 @@ import {
conferenceJoined,
conferenceLeft,
conferenceSubjectChanged,
conferenceTimestampChanged,
conferenceWillJoin,
conferenceWillLeave,
dataChannelOpened,
@ -1818,7 +1819,10 @@ export default {
room.on(
JitsiConferenceEvents.CONFERENCE_LEFT,
(...args) => APP.store.dispatch(conferenceLeft(room, ...args)));
(...args) => {
APP.store.dispatch(conferenceTimestampChanged(0));
APP.store.dispatch(conferenceLeft(room, ...args));
});
room.on(
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
@ -1948,6 +1952,10 @@ export default {
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
id => APP.store.dispatch(dominantSpeakerChanged(id, room)));
room.on(
JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
conferenceTimestamp => APP.store.dispatch(conferenceTimestampChanged(conferenceTimestamp)));
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
APP.store.dispatch(localParticipantConnectionStatusChanged(
JitsiParticipantConnectionStatus.INTERRUPTED));

@ -23,4 +23,10 @@
&-text {
vertical-align: middle;
}
&-conference-timer {
display: block;
font-size: 15px;
opacity: 0.6;
}
}

@ -30,6 +30,7 @@ VirtualHost "jitmeet.example.com"
certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
}
speakerstats_component = "speakerstats.jitmeet.example.com"
conference_duration_component = "conference_duration.jitmeet.example.com"
-- we need bosh
modules_enabled = {
"bosh";
@ -37,6 +38,7 @@ VirtualHost "jitmeet.example.com"
"ping"; -- Enable mod_ping
"speakerstats";
"turncredentials";
"conference_duration";
}
c2s_require_encryption = false
@ -65,3 +67,6 @@ Component "focus.jitmeet.example.com"
Component "speakerstats.jitmeet.example.com" "speakerstats_component"
muc_component = "conference.jitmeet.example.com"
Component "conference_duration.jitmeet.example.com" "conference_duration_component"
muc_component = "conference.jitmeet.example.com"

4
package-lock.json generated

@ -10869,8 +10869,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#9ba4c15e85c6b270c367affab07180a069041ae9",
"from": "github:jitsi/lib-jitsi-meet#9ba4c15e85c6b270c367affab07180a069041ae9",
"version": "github:jitsi/lib-jitsi-meet#f35c74222f644b6420cddd8f6a4a2dfffe451f3b",
"from": "github:jitsi/lib-jitsi-meet#f35c74222f644b6420cddd8f6a4a2dfffe451f3b",
"requires": {
"@jitsi/sdp-interop": "0.1.14",
"@jitsi/sdp-simulcast": "0.2.2",

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

@ -52,6 +52,16 @@ export const CONFERENCE_LEFT = 'CONFERENCE_LEFT';
*/
export const CONFERENCE_SUBJECT_CHANGED = 'CONFERENCE_SUBJECT_CHANGED';
/**
* The type of (redux) action, which indicates conference UTC timestamp changes.
*
* {
* type: CONFERENCE_TIMESTAMP_CHANGED
* timestamp: number
* }
*/
export const CONFERENCE_TIMESTAMP_CHANGED = 'CONFERENCE_TIMESTAMP_CHANGED';
/**
* The type of (redux) action which signals that a specific conference will be
* joined.

@ -35,6 +35,7 @@ import {
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_SUBJECT_CHANGED,
CONFERENCE_TIMESTAMP_CHANGED,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED,
@ -93,10 +94,16 @@ function _addConferenceListeners(conference, dispatch) {
(...args) => dispatch(conferenceJoined(conference, ...args)));
conference.on(
JitsiConferenceEvents.CONFERENCE_LEFT,
(...args) => dispatch(conferenceLeft(conference, ...args)));
(...args) => {
dispatch(conferenceTimestampChanged(0));
dispatch(conferenceLeft(conference, ...args));
});
conference.on(JitsiConferenceEvents.SUBJECT_CHANGED,
(...args) => dispatch(conferenceSubjectChanged(...args)));
conference.on(JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
(...args) => dispatch(conferenceTimestampChanged(...args)));
conference.on(
JitsiConferenceEvents.KICKED,
(...args) => dispatch(kickedOut(conference, ...args)));
@ -313,6 +320,22 @@ export function conferenceSubjectChanged(subject: string) {
};
}
/**
* Signals that the conference timestamp has been changed.
*
* @param {number} conferenceTimestamp - The UTC timestamp.
* @returns {{
* type: CONFERENCE_TIMESTAMP_CHANGED,
* conferenceTimestamp
* }}
*/
export function conferenceTimestampChanged(conferenceTimestamp: number) {
return {
type: CONFERENCE_TIMESTAMP_CHANGED,
conferenceTimestamp
};
}
/**
* Adds any existing local tracks to a specific conference before the conference
* is joined. Then signals the intention of the application to have the local

@ -167,6 +167,20 @@ export function getConferenceName(stateful: Function | Object): string {
|| _.startCase(safeDecodeURIComponent(room));
}
/**
* Returns the UTC timestamp when the first participant joined the conference.
*
* @param {Function | Object} stateful - Reference that can be resolved to Redux
* state with the {@code toState} function.
* @returns {number}
*/
export function getConferenceTimestamp(stateful: Function | Object): number {
const state = toState(stateful);
const { conferenceTimestamp } = state['features/base/conference'];
return conferenceTimestamp;
}
/**
* Returns the current {@code JitsiConference} which is joining or joined and is
* not leaving. Please note the contrast with merely reading the

@ -11,6 +11,7 @@ import {
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_SUBJECT_CHANGED,
CONFERENCE_TIMESTAMP_CHANGED,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
LOCK_STATE_CHANGED,
@ -59,6 +60,9 @@ ReducerRegistry.register(
case CONFERENCE_SUBJECT_CHANGED:
return set(state, 'subject', action.subject);
case CONFERENCE_TIMESTAMP_CHANGED:
return set(state, 'conferenceTimestamp', action.conferenceTimestamp);
case CONFERENCE_LEFT:
case CONFERENCE_WILL_LEAVE:
return _conferenceLeftOrWillLeave(state, action);

@ -0,0 +1,176 @@
// @flow
import { Component } from 'react';
import { connect } from '../../base/redux';
import { getLocalizedDurationFormatter } from '../../base/i18n';
import { getConferenceTimestamp } from '../../base/conference/functions';
import { renderConferenceTimer } from '../';
/**
* The type of the React {@code Component} props of {@link ConferenceTimer}.
*/
type Props = {
/**
* The UTC timestamp representing the time when first participant joined.
*/
_startTimestamp: ?number,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
/**
* The type of the React {@code Component} state of {@link ConferenceTimer}.
*/
type State = {
/**
* Value of current conference time.
*/
timerValue: string
};
/**
* ConferenceTimer react component.
*
* @class ConferenceTimer
* @extends Component
*/
class ConferenceTimer extends Component<Props, State> {
/**
* Handle for setInterval timer.
*/
_interval;
/**
* Initializes a new {@code ConferenceTimer} instance.
*
* @param {Props} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
timerValue: getLocalizedDurationFormatter(0)
};
}
/**
* Starts the conference timer when component will be
* mounted.
*
* @inheritdoc
*/
componentDidMount() {
this._startTimer();
}
/**
* Stops the conference timer when component will be
* unmounted.
*
* @inheritdoc
*/
componentWillUnmount() {
this._stopTimer();
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { timerValue } = this.state;
const { _startTimestamp } = this.props;
if (!_startTimestamp) {
return null;
}
return renderConferenceTimer(timerValue);
}
/**
* Sets the current state values that will be used to render the timer.
*
* @param {number} refValueUTC - The initial UTC timestamp value.
* @param {number} currentValueUTC - The current UTC timestamp value.
*
* @returns {void}
*/
_setStateFromUTC(refValueUTC, currentValueUTC) {
if (!refValueUTC || !currentValueUTC) {
return;
}
if (currentValueUTC < refValueUTC) {
return;
}
const timerMsValue = currentValueUTC - refValueUTC;
const localizedTime = getLocalizedDurationFormatter(timerMsValue);
this.setState({
timerValue: localizedTime
});
}
/**
* Start conference timer.
*
* @returns {void}
*/
_startTimer() {
if (!this._interval) {
this._setStateFromUTC(this.props._startTimestamp, (new Date()).getTime());
this._interval = setInterval(() => {
this._setStateFromUTC(this.props._startTimestamp, (new Date()).getTime());
}, 1000);
}
}
/**
* Stop conference timer.
*
* @returns {void}
*/
_stopTimer() {
if (this._interval) {
clearInterval(this._interval);
}
this.setState({
timerValue: getLocalizedDurationFormatter(0)
});
}
}
/**
* Maps (parts of) the Redux state to the associated
* {@code ConferenceTimer}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _startTimestamp: number
* }}
*/
export function _mapStateToProps(state: Object) {
return {
_startTimestamp: getConferenceTimestamp(state)
};
}
export default connect(_mapStateToProps)(ConferenceTimer);

@ -0,0 +1,23 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import styles from './styles';
/**
* Returns native element to be rendered.
*
* @param {string} timerValue - String to display as time.
*
* @returns {ReactElement}
*/
export default function renderConferenceTimer(timerValue: string) {
return (
<Text
numberOfLines = { 4 }
style = { styles.roomTimer }>
{ timerValue }
</Text>
);
}

@ -9,6 +9,7 @@ import { connect } from '../../../base/redux';
import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
import { isToolboxVisible } from '../../../toolbox';
import ConferenceTimer from '../ConferenceTimer';
import styles, { NAVBAR_GRADIENT_COLORS } from './styles';
type Props = {
@ -63,6 +64,7 @@ class NavigationBar extends Component<Props> {
style = { styles.roomName }>
{ this.props._meetingName }
</Text>
<ConferenceTimer />
</View>
</View>
];

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

@ -105,6 +105,12 @@ export default {
paddingHorizontal: 14
},
roomTimer: {
color: ColorPalette.white,
fontSize: 15,
opacity: 0.6
},
roomName: {
color: ColorPalette.white,
fontSize: 17,
@ -112,8 +118,8 @@ export default {
},
roomNameWrapper: {
flexDirection: 'row',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
left: 0,
paddingHorizontal: 48,
position: 'absolute',

@ -0,0 +1,16 @@
// @flow
import React from 'react';
/**
* Returns web element to be rendered.
*
* @param {string} timerValue - String to display as time.
*
* @returns {ReactElement}
*/
export default function renderConferenceTimer(timerValue: string) {
return (
<span className = 'subject-conference-timer' >{ timerValue }</span>
);
}

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

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

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

@ -0,0 +1,66 @@
local st = require "util.stanza";
local socket = require "socket";
local json = require "util.json";
local ext_events = module:require "ext_events";
local it = require "util.iterators";
-- we use async to detect Prosody 0.10 and earlier
local have_async = pcall(require, "util.async");
if not have_async then
module:log("warn", "conference duration will not work with Prosody version 0.10 or less.");
return;
end
local muc_component_host = module:get_option_string("muc_component");
if muc_component_host == nil then
log("error", "No muc_component specified. No muc to operate on!");
return;
end
log("info", "Starting conference duration timer for %s", muc_component_host);
function occupant_joined(event)
local room = event.room;
local occupant = event.occupant;
local participant_count = it.count(room:each_occupant());
if participant_count > 1 then
if room.created_timestamp == nil then
room.created_timestamp = os.time(os.date("!*t")) * 1000; -- Lua provides UTC time in seconds, so convert to milliseconds
end
local body_json = {};
body_json.type = 'conference_duration';
body_json.created_timestamp = room.created_timestamp;
local stanza = st.message({
from = module.host;
to = occupant.jid;
})
:tag("json-message", {xmlns='http://jitsi.org/jitmeet'})
:text(json.encode(body_json)):up();
room:route_stanza(stanza);
end
end
-- executed on every host added internally in prosody, including components
function process_host(host)
if host == muc_component_host then -- the conference muc component
module:log("info", "Hook to muc events on %s", host);
local muc_module = module:context(host)
muc_module:hook("muc-occupant-joined", occupant_joined, -1);
end
end
if prosody.hosts[muc_component_host] == nil then
module:log("info", "No muc component found, will listen for it: %s", muc_component_host);
-- when a host or component is added
prosody.events.add_handler("host-activated", process_host);
else
process_host(muc_component_host);
end
Loading…
Cancel
Save