Outgoing call ringtones (#2949)
* fix(PresenceLabel): Use translated strings for the presence label. * feat(sounds): Implements loop and stop functionality. * feat(invite): Add ringtones. * fix(invite): Code style issues.
This commit is contained in:
parent
ee74f11c3d
commit
c344a83376
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
<Provider store = { APP.store }>
|
||||
<PresenceLabel participantID = { id } />
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<PresenceLabel participantID = { id } />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
presenceLabelContainer.get(0));
|
||||
}
|
||||
|
|
|
@ -609,7 +609,9 @@ RemoteVideo.prototype.addPresenceLabel = function() {
|
|||
if (presenceLabelContainer) {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<PresenceLabel participantID = { this.id } />
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<PresenceLabel participantID = { this.id } />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
presenceLabelContainer);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,8 +41,11 @@ export default class Audio extends AbstractAudio {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const loop = this.props.loop ? 'true' : null;
|
||||
|
||||
return (
|
||||
<audio
|
||||
loop = { loop }
|
||||
onCanPlayThrough = { this._onCanPlayThrough }
|
||||
preload = 'auto'
|
||||
|
||||
|
@ -52,6 +55,20 @@ export default class Audio extends AbstractAudio {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the audio HTML element.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
stop() {
|
||||
if (this._ref) {
|
||||
this._ref.pause();
|
||||
|
||||
// $FlowFixMe
|
||||
this._ref.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If audio element reference has been set and the file has been
|
||||
* loaded then {@link setAudioElementImpl} will be called to eventually add
|
||||
|
|
|
@ -42,6 +42,16 @@ export const PLAY_SOUND = Symbol('PLAY_SOUND');
|
|||
*/
|
||||
export const REGISTER_SOUND = Symbol('REGISTER_SOUND');
|
||||
|
||||
/**
|
||||
* The type of (redux) action to stop a sound from the sounds collection.
|
||||
*
|
||||
* {
|
||||
* type: STOP_SOUND,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const STOP_SOUND = Symbol('STOP_SOUND');
|
||||
|
||||
/**
|
||||
* The type of (redux) action to unregister an existing sound from the sounds
|
||||
* collection.
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
_REMOVE_AUDIO_ELEMENT,
|
||||
PLAY_SOUND,
|
||||
REGISTER_SOUND,
|
||||
STOP_SOUND,
|
||||
UNREGISTER_SOUND
|
||||
} from './actionTypes';
|
||||
import { getSoundsPath } from './functions';
|
||||
|
@ -84,17 +85,41 @@ export function playSound(soundId: string): Object {
|
|||
* created for given source object.
|
||||
* @param {string} soundName - The name of bundled audio file that will be
|
||||
* associated with the given {@code soundId}.
|
||||
* @param {Object} options - Optional paramaters.
|
||||
* @param {boolean} options.loop - True in order to loop the sound.
|
||||
* @returns {{
|
||||
* type: REGISTER_SOUND,
|
||||
* soundId: string,
|
||||
* src: string
|
||||
* src: string,
|
||||
* options: {
|
||||
* loop: boolean
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function registerSound(soundId: string, soundName: string): Object {
|
||||
export function registerSound(
|
||||
soundId: string, soundName: string, options: Object = {}): Object {
|
||||
return {
|
||||
type: REGISTER_SOUND,
|
||||
soundId,
|
||||
src: `${getSoundsPath()}/${soundName}`
|
||||
src: `${getSoundsPath()}/${soundName}`,
|
||||
options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops playback of the sound identified by the given sound id.
|
||||
*
|
||||
* @param {string} soundId - The id of the sound to be stopped (the same one
|
||||
* which was used in {@link registerSound} to register the sound).
|
||||
* @returns {{
|
||||
* type: STOP_SOUND,
|
||||
* soundId: string
|
||||
* }}
|
||||
*/
|
||||
export function stopSound(soundId: string): Object {
|
||||
return {
|
||||
type: STOP_SOUND,
|
||||
soundId
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -55,12 +55,15 @@ class SoundCollection extends Component<Props> {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -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 {
|
|||
<div
|
||||
className
|
||||
= { `presence-label ${_presence ? '' : 'no-presence'}` }>
|
||||
{ _presence }
|
||||
{ this._getPresenceText() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
|
|
|
@ -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<String, String>}
|
||||
*/
|
||||
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'
|
||||
};
|
|
@ -1 +1,2 @@
|
|||
export * from './components';
|
||||
export * from './constants';
|
||||
|
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue