diff --git a/lang/main.json b/lang/main.json index 80e64bf4fa..4803ab2234 100644 --- a/lang/main.json +++ b/lang/main.json @@ -577,5 +577,15 @@ "appNotInstalled": "You need the __app__ mobile app to join this meeting on your phone.", "downloadApp": "Download the app", "openApp": "Continue to the app" + }, + "presenceStatus": { + "invited": "Invited", + "ringing": "Ringing", + "calling": "Calling", + "connected": "Connected", + "connecting": "Connecting", + "busy": "Busy", + "rejected": "Rejected", + "ignored": "Ignored" } } diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index 9ddafa45ad..6d7ef3d62f 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -2,8 +2,10 @@ /* eslint-disable no-unused-vars */ import React from 'react'; import ReactDOM from 'react-dom'; +import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; +import { i18next } from '../../../react/features/base/i18n'; import { PresenceLabel } from '../../../react/features/presence-status'; /* eslint-enable no-unused-vars */ @@ -456,7 +458,9 @@ export default class LargeVideoManager { if (presenceLabelContainer.length) { ReactDOM.render( - + + + , presenceLabelContainer.get(0)); } diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 2986c4c0b2..150818f466 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -609,7 +609,9 @@ RemoteVideo.prototype.addPresenceLabel = function() { if (presenceLabelContainer) { ReactDOM.render( - + + + , presenceLabelContainer); } diff --git a/react/features/base/media/components/AbstractAudio.js b/react/features/base/media/components/AbstractAudio.js index 7919bcc669..28c3d68535 100644 --- a/react/features/base/media/components/AbstractAudio.js +++ b/react/features/base/media/components/AbstractAudio.js @@ -7,6 +7,7 @@ import { Component } from 'react'; * playback. */ export type AudioElement = { + currentTime?: number, pause: () => void, play: () => void, setSinkId?: string => void @@ -32,7 +33,8 @@ type Props = { * @type {Object | string} */ src: Object | string, - stream: Object + stream: Object, + loop?: ?boolean } /** diff --git a/react/features/base/media/components/native/Audio.js b/react/features/base/media/components/native/Audio.js index fa2ee4109a..418c75ce7e 100644 --- a/react/features/base/media/components/native/Audio.js +++ b/react/features/base/media/components/native/Audio.js @@ -74,4 +74,17 @@ export default class Audio extends AbstractAudio { // writing to not render anything. return null; } + + /** + * Stops the sound if it's currently playing. + * + * @returns {void} + */ + stop() { + // Currently not implemented for mobile. If needed, a possible + // implementation is: + // if (this._sound) { + // this._sound.stop(); + // } + } } diff --git a/react/features/base/media/components/web/Audio.js b/react/features/base/media/components/web/Audio.js index bfba1365a9..1fc56aa69f 100644 --- a/react/features/base/media/components/web/Audio.js +++ b/react/features/base/media/components/web/Audio.js @@ -41,8 +41,11 @@ export default class Audio extends AbstractAudio { * @returns {ReactElement} */ render() { + const loop = this.props.loop ? 'true' : null; + return ( { const sounds = []; for (const [ soundId, sound ] of this.props._sounds.entries()) { + const { options, src } = sound; + sounds.push( React.createElement( Audio, { key, setRef: this._setRef.bind(this, soundId), - src: sound.src + src, + loop: options.loop })); key += 1; } diff --git a/react/features/base/sounds/middleware.js b/react/features/base/sounds/middleware.js index d8c52f6ca8..3c3ffc002d 100644 --- a/react/features/base/sounds/middleware.js +++ b/react/features/base/sounds/middleware.js @@ -2,7 +2,7 @@ import { MiddlewareRegistry } from '../redux'; -import { PLAY_SOUND } from './actionTypes'; +import { PLAY_SOUND, STOP_SOUND } from './actionTypes'; const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -17,6 +17,9 @@ MiddlewareRegistry.register(store => next => action => { case PLAY_SOUND: _playSound(store, action.soundId); break; + case STOP_SOUND: + _stopSound(store, action.soundId); + break; } return next(action); @@ -44,3 +47,28 @@ function _playSound({ getState }, soundId) { logger.warn(`PLAY_SOUND: no sound found for id: ${soundId}`); } } + +/** + * Stop sound from audio element registered in the Redux store. + * + * @param {Store} store - The Redux store instance. + * @param {string} soundId - Audio element identifier. + * @private + * @returns {void} + */ +function _stopSound({ getState }, soundId) { + const sounds = getState()['features/base/sounds']; + const sound = sounds.get(soundId); + + if (sound) { + const { audioElement } = sound; + + if (audioElement) { + audioElement.stop(); + } else { + logger.warn(`STOP_SOUND: sound not loaded yet for id: ${soundId}`); + } + } else { + logger.warn(`STOP_SOUND: no sound found for id: ${soundId}`); + } +} diff --git a/react/features/base/sounds/reducer.js b/react/features/base/sounds/reducer.js index a6ccf0f2df..7e0fe5cafd 100644 --- a/react/features/base/sounds/reducer.js +++ b/react/features/base/sounds/reducer.js @@ -29,7 +29,12 @@ export type Sound = { * can be either a path to the file or an object depending on the platform * (native vs web). */ - src: Object | string + src: Object | string, + + /** + * This field is container for all optional parameters related to the sound. + */ + options: Object } /** @@ -115,7 +120,8 @@ function _registerSound(state, action) { const nextState = new Map(state); nextState.set(action.soundId, { - src: action.src + src: action.src, + options: action.options }); return nextState; diff --git a/react/features/invite/constants.js b/react/features/invite/constants.js new file mode 100644 index 0000000000..97ea8b93f5 --- /dev/null +++ b/react/features/invite/constants.js @@ -0,0 +1,14 @@ +/** + * The identifier of the sound to be played when the status of an outgoing call + * is ringing. + * + * @type {string} + */ +export const OUTGOING_CALL_RINGING_SOUND_ID = 'OUTGOING_CALL_RINGING_SOUND_ID'; + +/** + * The identifier of the sound to be played when outgoing call is started. + * + * @type {string} + */ +export const OUTGOING_CALL_START_SOUND_ID = 'OUTGOING_CALL_START_SOUND_ID'; diff --git a/react/features/invite/middleware.any.js b/react/features/invite/middleware.any.js index c2642313c2..a83061ca9b 100644 --- a/react/features/invite/middleware.any.js +++ b/react/features/invite/middleware.any.js @@ -1,11 +1,38 @@ // @flow +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; +import { + getParticipantById, + PARTICIPANT_UPDATED, + PARTICIPANT_LEFT +} from '../base/participants'; import { MiddlewareRegistry } from '../base/redux'; +import { + playSound, + registerSound, + stopSound, + unregisterSound +} from '../base/sounds'; +import { + CALLING, + INVITED, + RINGING +} from '../presence-status'; import { UPDATE_DIAL_IN_NUMBERS_FAILED } from './actionTypes'; +import { + OUTGOING_CALL_START_SOUND_ID, + OUTGOING_CALL_RINGING_SOUND_ID +} from './constants'; +import { + OUTGOING_CALL_START_FILE, + OUTGOING_CALL_RINGING_FILE +} from './sounds'; const logger = require('jitsi-meet-logger').getLogger(__filename); +declare var interfaceConfig: Object; + /** * The middleware of the feature invite common to mobile/react-native and * Web/React. @@ -13,11 +40,66 @@ const logger = require('jitsi-meet-logger').getLogger(__filename); * @param {Store} store - The redux store. * @returns {Function} */ -// eslint-disable-next-line no-unused-vars MiddlewareRegistry.register(store => next => action => { + let oldParticipantPresence; + + if (action.type === PARTICIPANT_UPDATED + || action.type === PARTICIPANT_LEFT) { + oldParticipantPresence + = _getParticipantPresence(store.getState(), action.participant.id); + } + const result = next(action); switch (action.type) { + case APP_WILL_MOUNT: + store.dispatch( + registerSound( + OUTGOING_CALL_START_SOUND_ID, + OUTGOING_CALL_START_FILE)); + + store.dispatch( + registerSound( + OUTGOING_CALL_RINGING_SOUND_ID, + OUTGOING_CALL_RINGING_FILE, + { loop: true })); + break; + + case APP_WILL_UNMOUNT: + store.dispatch(unregisterSound(OUTGOING_CALL_START_SOUND_ID)); + store.dispatch(unregisterSound(OUTGOING_CALL_RINGING_SOUND_ID)); + break; + + case PARTICIPANT_LEFT: + case PARTICIPANT_UPDATED: { + const newParticipantPresence + = _getParticipantPresence(store.getState(), action.participant.id); + + if (oldParticipantPresence === newParticipantPresence) { + break; + } + + switch (oldParticipantPresence) { + case CALLING: + case INVITED: + store.dispatch(stopSound(OUTGOING_CALL_START_SOUND_ID)); + break; + case RINGING: + store.dispatch(stopSound(OUTGOING_CALL_RINGING_SOUND_ID)); + break; + } + + switch (newParticipantPresence) { + case CALLING: + case INVITED: + store.dispatch(playSound(OUTGOING_CALL_START_SOUND_ID)); + break; + case RINGING: + store.dispatch(playSound(OUTGOING_CALL_RINGING_SOUND_ID)); + } + + break; + } case UPDATE_DIAL_IN_NUMBERS_FAILED: logger.error( 'Error encountered while fetching dial-in numbers:', @@ -27,3 +109,24 @@ MiddlewareRegistry.register(store => next => action => { return result; }); + +/** + * Returns the presence status of a participant associated with the passed id. + * + * @param {Object} state - The redux state. + * @param {string} id - The id of the participant. + * @returns {string} - The presence status. + */ +function _getParticipantPresence(state, id) { + if (!id) { + return undefined; + } + const participants = state['features/base/participants']; + const participantById = getParticipantById(participants, id); + + if (!participantById) { + return undefined; + } + + return participantById.presence; +} diff --git a/react/features/invite/sounds.js b/react/features/invite/sounds.js new file mode 100644 index 0000000000..43b512f04e --- /dev/null +++ b/react/features/invite/sounds.js @@ -0,0 +1,11 @@ +/** + * The name of the sound file which will be played when the status of an + * outgoing call is ringing. + */ +export const OUTGOING_CALL_RINGING_FILE = 'outgoingRinging.wav'; + +/** + * The name of the sound file which will be played when outgoing call is + * started. + */ +export const OUTGOING_CALL_START_FILE = 'outgoingStart.wav'; diff --git a/react/features/presence-status/components/PresenceLabel.js b/react/features/presence-status/components/PresenceLabel.js index 651f2df9f3..5282da3b13 100644 --- a/react/features/presence-status/components/PresenceLabel.js +++ b/react/features/presence-status/components/PresenceLabel.js @@ -2,8 +2,11 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; +import { translate } from '../../base/i18n'; import { getParticipantById } from '../../base/participants'; +import { STATUS_TO_I18N_KEY } from '../constants'; + /** * React {@code Component} for displaying the current presence status of a * participant. @@ -35,7 +38,12 @@ class PresenceLabel extends Component { /** * The ID of the participant whose presence status shoul display. */ - participantID: PropTypes.string + participantID: PropTypes.string, + + /** + * Invoked to obtain translated strings. + */ + t: PropTypes.func }; /** @@ -51,10 +59,31 @@ class PresenceLabel extends Component { - { _presence } + { this._getPresenceText() } ); } + + /** + * Returns the text associated with the current presence status. + * + * @returns {string} + */ + _getPresenceText() { + const { _presence, t } = this.props; + + if (!_presence) { + return null; + } + + const i18nKey = STATUS_TO_I18N_KEY[_presence]; + + if (!i18nKey) { // fallback to status value + return _presence; + } + + return t(i18nKey); + } } /** @@ -79,4 +108,4 @@ function _mapStateToProps(state, ownProps) { }; } -export default connect(_mapStateToProps)(PresenceLabel); +export default translate(connect(_mapStateToProps)(PresenceLabel)); diff --git a/react/features/presence-status/constants.js b/react/features/presence-status/constants.js new file mode 100644 index 0000000000..159b0673ee --- /dev/null +++ b/react/features/presence-status/constants.js @@ -0,0 +1,70 @@ +/** + * Тhe status for a participant when it's invited to a conference. + * + * @type {string} + */ +export const INVITED = 'Invited'; + +/** + * Тhe status for a participant when a call has been initiated. + * + * @type {string} + */ +export const CALLING = 'Calling'; + +/** + * Тhe status for a participant when the invite is received and its device(s) + * are ringing. + * + * @type {string} + */ +export const RINGING = 'Ringing'; + +/** + * A status for a participant that indicates the call is connected. + * NOTE: Currently used for phone numbers only. + * + * @type {string} + */ +export const CONNECTED = 'Connected'; + +/** + * A status for a participant that indicates the call is in process of + * connecting. + * NOTE: Currently used for phone numbers only. + * + * @type {string} + */ +export const CONNECTING = 'Connecting'; + +/** + * The status for a participant when the invitation is received but the user + * has responded with busy message. + */ +export const BUSY = 'Busy'; + +/** + * The status for a participant when the invitation is rejected. + */ +export const REJECTED = 'Rejected'; + +/** + * The status for a participant when the invitation is ignored. + */ +export const IGNORED = 'Ignored'; + +/** + * Maps the presence status values to i18n translation keys. + * + * @type {Object} + */ +export const STATUS_TO_I18N_KEY = { + 'Invited': 'presenceStatus.invited', + 'Ringing': 'presenceStatus.ringing', + 'Calling': 'presenceStatus.calling', + 'Connected': 'presenceStatus.connected', + 'Connecting': 'presenceStatus.connecting', + 'Busy': 'presenceStatus.busy', + 'Rejected': 'presenceStatus.rejected', + 'Ignored': 'presenceStatus.ignored' +}; diff --git a/react/features/presence-status/index.js b/react/features/presence-status/index.js index 07635cbbc8..b6ad3a8a4b 100644 --- a/react/features/presence-status/index.js +++ b/react/features/presence-status/index.js @@ -1 +1,2 @@ export * from './components'; +export * from './constants'; diff --git a/sounds/outgoingRinging.wav b/sounds/outgoingRinging.wav new file mode 100644 index 0000000000..7a8b643a32 Binary files /dev/null and b/sounds/outgoingRinging.wav differ diff --git a/sounds/outgoingStart.wav b/sounds/outgoingStart.wav new file mode 100644 index 0000000000..4b60654876 Binary files /dev/null and b/sounds/outgoingStart.wav differ