Add conference timer (#4958)
This commit is contained in:
parent
c73ba37202
commit
c2cf09a2ca
|
@ -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"
|
||||
|
|
|
@ -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…
Reference in New Issue