mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-04-09 12:20:20 +00:00
Compare commits
11 Commits
2979
...
base_sessi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2191e3a28 | ||
|
|
72e3e8593d | ||
|
|
67a8b4915d | ||
|
|
468d4a7150 | ||
|
|
2a01e29fec | ||
|
|
90a64d30dc | ||
|
|
31905d4f63 | ||
|
|
7e1d97665a | ||
|
|
b978851a0f | ||
|
|
ef49817eaf | ||
|
|
cac8888b37 |
@@ -45,6 +45,7 @@ import {
|
||||
setDesktopSharingEnabled
|
||||
} from './react/features/base/conference';
|
||||
import {
|
||||
getAvailableDevices,
|
||||
setAudioOutputDeviceId,
|
||||
updateDeviceList
|
||||
} from './react/features/base/devices';
|
||||
@@ -2129,20 +2130,6 @@ export default {
|
||||
}
|
||||
);
|
||||
|
||||
APP.UI.addListener(
|
||||
UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
|
||||
audioOutputDeviceId => {
|
||||
sendAnalytics(createDeviceChangedEvent('audio', 'output'));
|
||||
setAudioOutputDeviceId(audioOutputDeviceId, APP.store.dispatch)
|
||||
.then(() => logger.log('changed audio output device'))
|
||||
.catch(err => {
|
||||
logger.warn('Failed to change audio output device. '
|
||||
+ 'Default or previously set audio output device '
|
||||
+ 'will be used instead.', err);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
APP.UI.addListener(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly => {
|
||||
|
||||
// FIXME On web video track is stored both in redux and in
|
||||
@@ -2314,39 +2301,43 @@ export default {
|
||||
/**
|
||||
* Inits list of current devices and event listener for device change.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_initDeviceList() {
|
||||
const { mediaDevices } = JitsiMeetJS;
|
||||
|
||||
if (mediaDevices.isDeviceListAvailable()
|
||||
&& mediaDevices.isDeviceChangeAvailable()) {
|
||||
mediaDevices.enumerateDevices(devices => {
|
||||
// Ugly way to synchronize real device IDs with local storage
|
||||
// and settings menu. This is a workaround until
|
||||
// getConstraints() method will be implemented in browsers.
|
||||
const { dispatch } = APP.store;
|
||||
|
||||
if (this.localAudio) {
|
||||
dispatch(updateSettings({
|
||||
micDeviceId: this.localAudio.getDeviceId()
|
||||
}));
|
||||
}
|
||||
if (this.localVideo) {
|
||||
dispatch(updateSettings({
|
||||
cameraDeviceId: this.localVideo.getDeviceId()
|
||||
}));
|
||||
}
|
||||
|
||||
APP.store.dispatch(updateDeviceList(devices));
|
||||
APP.UI.onAvailableDevicesChanged(devices);
|
||||
});
|
||||
|
||||
this.deviceChangeListener = devices =>
|
||||
window.setTimeout(() => this._onDeviceListChanged(devices), 0);
|
||||
mediaDevices.addEventListener(
|
||||
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
|
||||
this.deviceChangeListener);
|
||||
|
||||
const { dispatch } = APP.store;
|
||||
|
||||
return dispatch(getAvailableDevices())
|
||||
.then(devices => {
|
||||
// Ugly way to synchronize real device IDs with local
|
||||
// storage and settings menu. This is a workaround until
|
||||
// getConstraints() method will be implemented in browsers.
|
||||
if (this.localAudio) {
|
||||
dispatch(updateSettings({
|
||||
micDeviceId: this.localAudio.getDeviceId()
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.localVideo) {
|
||||
dispatch(updateSettings({
|
||||
cameraDeviceId: this.localVideo.getDeviceId()
|
||||
}));
|
||||
}
|
||||
|
||||
APP.UI.onAvailableDevicesChanged(devices);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
AspectRatioDetector,
|
||||
ReducedUIDetector
|
||||
} from '../../base/responsive-ui';
|
||||
import '../../google-api';
|
||||
import '../../mobile/audio-mode';
|
||||
import '../../mobile/background';
|
||||
import '../../mobile/callkit';
|
||||
|
||||
@@ -37,6 +37,9 @@ const INITIAL_RN_STATE = {
|
||||
// fastest to merely disable them.
|
||||
disableAudioLevels: true,
|
||||
|
||||
// FIXME flow complains about missing 'locationURL' missing in _setConfig
|
||||
locationURL: undefined,
|
||||
|
||||
p2p: {
|
||||
disableH264: false,
|
||||
preferH264: true
|
||||
@@ -126,8 +129,10 @@ function _setConfig(state, { config }) {
|
||||
|
||||
const newState = _.merge(
|
||||
{},
|
||||
config,
|
||||
{ error: undefined },
|
||||
config, {
|
||||
error: undefined,
|
||||
locationURL: state.locationURL
|
||||
},
|
||||
|
||||
// The config of _getInitialState() is meant to override the config
|
||||
// downloaded from the Jitsi Meet deployment because the former contains
|
||||
|
||||
@@ -9,17 +9,6 @@
|
||||
*/
|
||||
export const SET_AUDIO_INPUT_DEVICE = Symbol('SET_AUDIO_INPUT_DEVICE');
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the currently used audio
|
||||
* output device should be changed.
|
||||
*
|
||||
* {
|
||||
* type: SET_AUDIO_OUTPUT_DEVICE,
|
||||
* deviceId: string,
|
||||
* }
|
||||
*/
|
||||
export const SET_AUDIO_OUTPUT_DEVICE = Symbol('SET_AUDIO_OUTPUT_DEVICE');
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the currently used video
|
||||
* input device should be changed.
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
|
||||
import {
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_AUDIO_OUTPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Queries for connected A/V input and output devices and updates the redux
|
||||
* state of known devices.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function getAvailableDevices() {
|
||||
return dispatch => new Promise(resolve => {
|
||||
const { mediaDevices } = JitsiMeetJS;
|
||||
|
||||
if (mediaDevices.isDeviceListAvailable()
|
||||
&& mediaDevices.isDeviceChangeAvailable()) {
|
||||
mediaDevices.enumerateDevices(devices => {
|
||||
dispatch(updateDeviceList(devices));
|
||||
|
||||
resolve(devices);
|
||||
});
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the currently used audio input device.
|
||||
*
|
||||
@@ -21,22 +45,6 @@ export function setAudioInputDevice(deviceId) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the currently used audio output device.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new audio ouput device.
|
||||
* @returns {{
|
||||
* type: SET_AUDIO_OUTPUT_DEVICE,
|
||||
* deviceId: string
|
||||
* }}
|
||||
*/
|
||||
export function setAudioOutputDevice(deviceId) {
|
||||
return {
|
||||
type: SET_AUDIO_OUTPUT_DEVICE,
|
||||
deviceId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the currently used video input device.
|
||||
*
|
||||
|
||||
@@ -6,7 +6,6 @@ import { MiddlewareRegistry } from '../redux';
|
||||
|
||||
import {
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_AUDIO_OUTPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE
|
||||
} from './actionTypes';
|
||||
|
||||
@@ -22,9 +21,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case SET_AUDIO_INPUT_DEVICE:
|
||||
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
|
||||
break;
|
||||
case SET_AUDIO_OUTPUT_DEVICE:
|
||||
APP.UI.emitEvent(UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, action.deviceId);
|
||||
break;
|
||||
case SET_VIDEO_INPUT_DEVICE:
|
||||
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_AUDIO_OUTPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
@@ -40,7 +39,6 @@ ReducerRegistry.register(
|
||||
// now.
|
||||
case SET_AUDIO_INPUT_DEVICE:
|
||||
case SET_VIDEO_INPUT_DEVICE:
|
||||
case SET_AUDIO_OUTPUT_DEVICE:
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,28 @@ class DialogWithTabs extends Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the props to pass into the tab component.
|
||||
*
|
||||
* @param {number} tabId - The index of the tab configuration within
|
||||
* {@link this.state.tabStates}.
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getTabProps(tabId) {
|
||||
const { tabs } = this.props;
|
||||
const { tabStates } = this.state;
|
||||
const tabConfiguration = tabs[tabId];
|
||||
const currentTabState = tabStates[tabId];
|
||||
|
||||
if (tabConfiguration.propsUpdateFunction) {
|
||||
return tabConfiguration.propsUpdateFunction(
|
||||
currentTabState,
|
||||
tabConfiguration.props);
|
||||
}
|
||||
|
||||
return { ...currentTabState };
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the tabs from the tab information passed on props.
|
||||
*
|
||||
@@ -155,10 +177,11 @@ class DialogWithTabs extends Component<Props, State> {
|
||||
<div className = { styles }>
|
||||
<TabComponent
|
||||
closeDialog = { closeDialog }
|
||||
mountCallback = { this.props.tabs[tabId].onMount }
|
||||
onTabStateChange
|
||||
= { this._onTabStateChange }
|
||||
tabId = { tabId }
|
||||
{ ...this.state.tabStates[tabId] } />
|
||||
{ ...this._getTabProps(tabId) } />
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
14
react/features/base/session/actionTypes.js
Normal file
14
react/features/base/session/actionTypes.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* {
|
||||
* type: SET_SESSION,
|
||||
* session: {
|
||||
* url: {string},
|
||||
* state: {string},
|
||||
* ...data
|
||||
* }
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const SET_SESSION = Symbol('SET_SESSION');
|
||||
16
react/features/base/session/actions.js
Normal file
16
react/features/base/session/actions.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { SET_SESSION } from './actionTypes';
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {string} session - FIXME.
|
||||
* @returns {{
|
||||
* type: SET_SESSION
|
||||
* }}
|
||||
*/
|
||||
export function setSession(session) {
|
||||
return {
|
||||
type: SET_SESSION,
|
||||
session
|
||||
};
|
||||
}
|
||||
12
react/features/base/session/constants.js
Normal file
12
react/features/base/session/constants.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
export const SESSION_CONFIGURED = Symbol('SESSION_CONFIGURED');
|
||||
|
||||
export const SESSION_ENDED = Symbol('SESSION_ENDED');
|
||||
|
||||
export const SESSION_FAILED = Symbol('SESSION_FAILED');
|
||||
|
||||
export const SESSION_STARTED = Symbol('SESSION_STARTED');
|
||||
|
||||
export const SESSION_WILL_END = Symbol('SESSION_WILL_END');
|
||||
|
||||
export const SESSION_WILL_START = Symbol('SESSION_WILL_START');
|
||||
36
react/features/base/session/functions.js
Normal file
36
react/features/base/session/functions.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// @flow
|
||||
|
||||
import { toState } from '../redux';
|
||||
import { toURLString } from '../util';
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Function|Object} stateful - FIXME.
|
||||
* @param {string} url - FIXME.
|
||||
* @returns {*}
|
||||
*/
|
||||
export function getSession(stateful: Function | Object, url: string): ?Object {
|
||||
const state = toState(stateful);
|
||||
|
||||
const session = state['features/base/session'].get(url);
|
||||
|
||||
if (!session) {
|
||||
console.info(`SESSION NOT FOUND FOR URL: ${url}`);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Function | Object} stateful - FIXME.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getCurrentSession(stateful: Function | Object): ?Object {
|
||||
const state = toState(stateful);
|
||||
const { locationURL } = state['features/base/config'];
|
||||
|
||||
return getSession(state, toURLString(locationURL));
|
||||
}
|
||||
7
react/features/base/session/index.js
Normal file
7
react/features/base/session/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './constants';
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
349
react/features/base/session/middleware.js
Normal file
349
react/features/base/session/middleware.js
Normal file
@@ -0,0 +1,349 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
isRoomValid
|
||||
} from '../../base/conference';
|
||||
import {
|
||||
CONNECTION_DISCONNECTED,
|
||||
CONNECTION_FAILED,
|
||||
CONNECTION_WILL_CONNECT
|
||||
} from '../../base/connection';
|
||||
import {
|
||||
MiddlewareRegistry,
|
||||
toState
|
||||
} from '../../base/redux';
|
||||
import { parseURIString, toURLString } from '../../base/util';
|
||||
|
||||
import {
|
||||
SESSION_CONFIGURED,
|
||||
SESSION_ENDED,
|
||||
SESSION_FAILED,
|
||||
SESSION_STARTED,
|
||||
SESSION_WILL_END,
|
||||
SESSION_WILL_START
|
||||
} from './constants';
|
||||
import { setSession } from './actions';
|
||||
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from '../config';
|
||||
import { getCurrentSession, getSession } from './functions';
|
||||
|
||||
/**
|
||||
* Middleware that maintains conference sessions. The features spans across
|
||||
* three features strictly related to the conference lifecycle.
|
||||
* The first one is the base/config which configures the session. It's
|
||||
* 'locationURL' state is used to tell what's the current conference URL the app
|
||||
* is working with. The session starts as soon as {@link CONFIG_WILL_LOAD} event
|
||||
* arrives. The {@code locationURL} instance is stored in the session to
|
||||
* associate the load config request with the session and be able to distinguish
|
||||
* between the current and outdated load config request failures. After the
|
||||
* config is loaded the lifecycle moves to the base/connection feature which
|
||||
* creates a {@code JitsiConnection} and tries to connect to the server. On
|
||||
* {@code CONNECTION_WILL_CONNECT} the connection instance is stored in the
|
||||
* session and used later to filter the events similar to what's done for
|
||||
* the load config requests. The base/conference feature adds the last part to
|
||||
* the session's lifecycle. A {@code JitsiConference} instance is stored in the
|
||||
* session on the {@code CONFERENCE_WILL_JOIN} action. A session is considered
|
||||
* alive as long as either connection or conference is available and
|
||||
* operational.
|
||||
*
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const result = next(action);
|
||||
const { type } = action;
|
||||
|
||||
switch (type) {
|
||||
case CONFERENCE_WILL_JOIN: {
|
||||
const { conference } = action;
|
||||
const { locationURL } = store.getState()['features/base/connection'];
|
||||
const url = toURLString(locationURL);
|
||||
const session = getSession(store, url);
|
||||
|
||||
if (session) {
|
||||
store.dispatch(setSession({
|
||||
url: session.url,
|
||||
conference
|
||||
}));
|
||||
} else {
|
||||
console.info(`IGNORED WILL_JOIN FOR: ${url}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_JOINED: {
|
||||
const { conference } = action;
|
||||
const session = findSessionForConference(store, conference);
|
||||
const state = session && session.state;
|
||||
|
||||
if (state === SESSION_CONFIGURED) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
|
||||
// Flow complains that the session can be undefined, but it
|
||||
// can't if the state is defined.
|
||||
// $FlowExpectedError
|
||||
url: session.url,
|
||||
state: SESSION_STARTED
|
||||
}));
|
||||
} else {
|
||||
// eslint-disable-next-line max-len
|
||||
console.info(`IGNORED CONF JOINED FOR: ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_LEFT:
|
||||
case CONFERENCE_FAILED: {
|
||||
const { conference, error } = action;
|
||||
const session = findSessionForConference(store, conference);
|
||||
|
||||
// FIXME update comments
|
||||
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
|
||||
// prevented the user from joining a specific conference but the app may
|
||||
// be able to eventually join the conference. For example, the app will
|
||||
// ask the user for a password upon
|
||||
// JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
|
||||
// conference afterwards. Such errors are to not reach the native
|
||||
// counterpart of the External API (or at least not in the
|
||||
// fatality/finality semantics attributed to
|
||||
// conferenceFailed:/onConferenceFailed).
|
||||
if (session) {
|
||||
if (!error || isGameOver(store, session, error)) {
|
||||
if (session.connection) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
conference: undefined
|
||||
}));
|
||||
} else {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
state: error ? SESSION_FAILED : SESSION_ENDED,
|
||||
error
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line max-len
|
||||
console.info(`IGNORED FAILED/LEFT for ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`, error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// NOTE WILL_JOIN is fired on SET_ROOM
|
||||
// case CONFERENCE_WILL_JOIN:
|
||||
case CONFERENCE_WILL_LEAVE: {
|
||||
const { conference } = action;
|
||||
const url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
|
||||
const session = findSessionForConference(store, conference);
|
||||
const state = session && session.state;
|
||||
|
||||
if (state && state !== SESSION_WILL_END) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
|
||||
// Flow complains that the session can be undefined, but it
|
||||
// can't if the state is defined.
|
||||
// $FlowExpectedError
|
||||
url: session.url,
|
||||
state: SESSION_WILL_END
|
||||
}));
|
||||
} else {
|
||||
console.info(`IGNORED WILL LEAVE FOR ${url}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONNECTION_WILL_CONNECT: {
|
||||
const { connection } = action;
|
||||
const { locationURL } = store.getState()['features/base/connection'];
|
||||
const url = toURLString(locationURL);
|
||||
const session = getSession(store, url);
|
||||
|
||||
if (session) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
connection,
|
||||
conference: undefined // Detach from the old conference
|
||||
}));
|
||||
} else {
|
||||
console.info(`IGNORED CONNECTION_WILL_CONNECT FOR: ${url}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONNECTION_DISCONNECTED:
|
||||
case CONNECTION_FAILED: {
|
||||
const { connection, error } = action;
|
||||
const session = findSessionForConnection(store, connection);
|
||||
|
||||
if (session) {
|
||||
// Remove connection from the session, but wait for
|
||||
// the conference to be removed as well.
|
||||
if (!error || isGameOver(store, session, error)) {
|
||||
if (session.conference) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
connection: undefined
|
||||
}));
|
||||
} else {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
state: error ? SESSION_FAILED : SESSION_ENDED,
|
||||
error
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.info('Ignored DISCONNECTED/FAILED for connection');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_CONFIG: {
|
||||
// XXX SET_CONFIG IS ALWAYS RELEVANT
|
||||
const { locationURL } = store.getState()['features/base/config'];
|
||||
const url = toURLString(locationURL);
|
||||
const session = getSession(store, url);
|
||||
const state = session && session.state;
|
||||
|
||||
if (state === SESSION_WILL_START) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url,
|
||||
state: SESSION_CONFIGURED
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFIG_WILL_LOAD: {
|
||||
const { locationURL } = action;
|
||||
const url = toURLString(locationURL);
|
||||
const session = getSession(store, url);
|
||||
|
||||
// The back and forth to string conversion is here, because there's no
|
||||
// guarantee that the locationURL will be the exact custom structure
|
||||
// which contains the room property.
|
||||
let { room } = parseURIString(url);
|
||||
|
||||
// Validate the room
|
||||
room = isRoomValid(room) ? room : undefined;
|
||||
|
||||
if (room && !session) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url,
|
||||
state: SESSION_WILL_START,
|
||||
locationURL,
|
||||
room
|
||||
}));
|
||||
} else if (room && session) {
|
||||
// Update to the new locationURL instance
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url,
|
||||
locationURL
|
||||
}));
|
||||
} else {
|
||||
console.info(`IGNORED CFG WILL LOAD FOR ${url}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case LOAD_CONFIG_ERROR: {
|
||||
const { error, locationURL } = action;
|
||||
const url = toURLString(locationURL);
|
||||
const session = getSession(store, url);
|
||||
|
||||
if (session && session.locationURL === locationURL) {
|
||||
if (isGameOver(store, session, error)) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url,
|
||||
state: SESSION_FAILED,
|
||||
error
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
console.info(`IGNORED LOAD_CONFIG_ERROR FOR: ${url}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* FIXME A session is to be terminated either when the recoverable flag is
|
||||
* explicitly set to {@code false} or if the error arrives for a session which
|
||||
* is no longer current (the app has started working with another session).
|
||||
* This can happen when a conference which is being disconnected fails in which
|
||||
* case the session needs to be ended even if the flag is not {@code false}
|
||||
* because we know that there's no fatal error handling. This is kind of
|
||||
* a contract between the fatal error feature and the session which probably
|
||||
* indicates that the fatal error detection and handling should be incorporated
|
||||
* into the session feature.
|
||||
*
|
||||
* @param {Object | Function} stateful - FIXME.
|
||||
* @param {Object} session - FIXME.
|
||||
* @param {Object} error - FIXME.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isGameOver(stateful, session, error) {
|
||||
return getCurrentSession(stateful) !== session
|
||||
|| error.recoverable === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Object | Function} stateful - FIXME.
|
||||
* @param {JitsiConnection} connection - FIXME.
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
function findSessionForConnection(stateful, connection) {
|
||||
const state = toState(stateful);
|
||||
|
||||
for (const session of state['features/base/session'].values()) {
|
||||
if (session.connection === connection) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
console.info('Session not found for a connection');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Object | Function} stateful - FIXME.
|
||||
* @param {JitsiConference} conference - FIXME.
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
function findSessionForConference(stateful, conference) {
|
||||
const state = toState(stateful);
|
||||
|
||||
for (const session of state['features/base/session'].values()) {
|
||||
if (session.conference === conference) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
console.info('Session not found for a conference');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
71
react/features/base/session/reducer.js
Normal file
71
react/features/base/session/reducer.js
Normal file
@@ -0,0 +1,71 @@
|
||||
// @flow
|
||||
|
||||
import { assign, ReducerRegistry } from '../../base/redux';
|
||||
import { getSymbolDescription } from '../util';
|
||||
|
||||
import { SET_SESSION } from './actionTypes';
|
||||
import {
|
||||
SESSION_FAILED,
|
||||
SESSION_ENDED,
|
||||
SESSION_WILL_START
|
||||
} from './constants';
|
||||
|
||||
ReducerRegistry.register('features/base/session',
|
||||
(state = new Map(), action) => {
|
||||
switch (action.type) {
|
||||
case SET_SESSION:
|
||||
return _setSession(state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Object} featureState - FIXME.
|
||||
* @param {Object} action - FIXME.
|
||||
* @returns {Map<any, any>} - FIXME.
|
||||
* @private
|
||||
*/
|
||||
function _setSession(featureState, action) {
|
||||
const { url, state, ...data } = action.session;
|
||||
const session = featureState.get(url);
|
||||
const nextState = new Map(featureState);
|
||||
|
||||
// Drop the whole action if the url is not defined
|
||||
if (!url) {
|
||||
console.error('SET SESSION - NO URL');
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
if (session) {
|
||||
if (state === SESSION_ENDED || state === SESSION_FAILED) {
|
||||
nextState.delete(url);
|
||||
} else {
|
||||
nextState.set(
|
||||
url,
|
||||
assign(session, {
|
||||
url,
|
||||
state: state ? state : session.state,
|
||||
...data
|
||||
}));
|
||||
}
|
||||
} else if (state === SESSION_WILL_START) {
|
||||
nextState.set(
|
||||
url, {
|
||||
url,
|
||||
state,
|
||||
...data
|
||||
});
|
||||
}
|
||||
console.info(
|
||||
'SESSION STATE REDUCED: ',
|
||||
new Map(nextState),
|
||||
url,
|
||||
state && getSymbolDescription(state),
|
||||
action.session.error);
|
||||
|
||||
return nextState;
|
||||
}
|
||||
@@ -138,7 +138,7 @@ function _initSettings(featureState) {
|
||||
if (settings.audioOutputDeviceId
|
||||
!== JitsiMeetJS.mediaDevices.getAudioOutputDevice()) {
|
||||
JitsiMeetJS.mediaDevices.setAudioOutputDevice(
|
||||
audioOutputDeviceId
|
||||
settings.audioOutputDeviceId
|
||||
).catch(ex => {
|
||||
logger.warn('Failed to set audio output device from local '
|
||||
+ 'storage. Default audio output device will be used'
|
||||
|
||||
@@ -4,18 +4,22 @@ import {
|
||||
Transport
|
||||
} from '../../../modules/transport';
|
||||
|
||||
import { createDeviceChangedEvent, sendAnalytics } from '../analytics';
|
||||
import {
|
||||
getAudioOutputDeviceId,
|
||||
setAudioInputDevice,
|
||||
setAudioOutputDevice,
|
||||
setAudioOutputDeviceId,
|
||||
setVideoInputDevice
|
||||
} from '../base/devices';
|
||||
import { i18next } from '../base/i18n';
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
import { updateSettings } from '../base/settings';
|
||||
|
||||
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
|
||||
import { getDeviceSelectionDialogProps } from './functions';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Opens a popup window with the device selection dialog in it.
|
||||
*
|
||||
@@ -115,23 +119,22 @@ function _processRequest(dispatch, getState, request, responseCallback) { // esl
|
||||
responseCallback(getState()['features/base/devices']);
|
||||
break;
|
||||
case 'setDevice': {
|
||||
let action;
|
||||
const { device } = request;
|
||||
|
||||
switch (device.kind) {
|
||||
case 'audioinput':
|
||||
action = setAudioInputDevice;
|
||||
dispatch(setAudioInputDevice(device.id));
|
||||
break;
|
||||
case 'audiooutput':
|
||||
action = setAudioOutputDevice;
|
||||
setAudioOutputDeviceId(device.id, dispatch);
|
||||
break;
|
||||
case 'videoinput':
|
||||
action = setVideoInputDevice;
|
||||
dispatch(setVideoInputDevice(device.id));
|
||||
break;
|
||||
default:
|
||||
|
||||
}
|
||||
dispatch(action(device.id));
|
||||
|
||||
responseCallback(true);
|
||||
break;
|
||||
}
|
||||
@@ -179,6 +182,10 @@ export function submitDeviceSelectionTab(newState) {
|
||||
if (newState.selectedVideoInputId
|
||||
&& newState.selectedVideoInputId
|
||||
!== currentState.selectedVideoInputId) {
|
||||
dispatch(updateSettings({
|
||||
cameraDeviceId: newState.selectedVideoInputId
|
||||
}));
|
||||
|
||||
dispatch(
|
||||
setVideoInputDevice(newState.selectedVideoInputId));
|
||||
}
|
||||
@@ -186,6 +193,10 @@ export function submitDeviceSelectionTab(newState) {
|
||||
if (newState.selectedAudioInputId
|
||||
&& newState.selectedAudioInputId
|
||||
!== currentState.selectedAudioInputId) {
|
||||
dispatch(updateSettings({
|
||||
micDeviceId: newState.selectedAudioInputId
|
||||
}));
|
||||
|
||||
dispatch(
|
||||
setAudioInputDevice(newState.selectedAudioInputId));
|
||||
}
|
||||
@@ -193,8 +204,19 @@ export function submitDeviceSelectionTab(newState) {
|
||||
if (newState.selectedAudioOutputId
|
||||
&& newState.selectedAudioOutputId
|
||||
!== currentState.selectedAudioOutputId) {
|
||||
dispatch(
|
||||
setAudioOutputDevice(newState.selectedAudioOutputId));
|
||||
sendAnalytics(createDeviceChangedEvent('audio', 'output'));
|
||||
|
||||
setAudioOutputDeviceId(
|
||||
newState.selectedAudioOutputId,
|
||||
dispatch)
|
||||
.then(() => logger.log('changed audio output device'))
|
||||
.catch(err => {
|
||||
logger.warn(
|
||||
'Failed to change audio output device.',
|
||||
'Default or previously set audio output device will',
|
||||
' be used instead.',
|
||||
err);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import AudioOutputPreview from './AudioOutputPreview';
|
||||
import DeviceSelector from './DeviceSelector';
|
||||
import VideoInputPreview from './VideoInputPreview';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link DeviceSelection}.
|
||||
*/
|
||||
@@ -64,6 +66,12 @@ export type Props = {
|
||||
*/
|
||||
hideAudioOutputSelect: boolean,
|
||||
|
||||
/**
|
||||
* An optional callback to invoke after the component has completed its
|
||||
* mount logic.
|
||||
*/
|
||||
mountCallback?: Function,
|
||||
|
||||
/**
|
||||
* The id of the audio input device to preview.
|
||||
*/
|
||||
@@ -134,8 +142,12 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._createAudioInputTrack(this.props.selectedAudioInputId);
|
||||
this._createVideoInputTrack(this.props.selectedVideoInputId);
|
||||
Promise.all([
|
||||
this._createAudioInputTrack(this.props.selectedAudioInputId),
|
||||
this._createVideoInputTrack(this.props.selectedVideoInputId)
|
||||
])
|
||||
.catch(err => logger.warn('Failed to initialize preview tracks', err))
|
||||
.then(() => this.props.mountCallback && this.props.mountCallback());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,7 +224,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_createAudioInputTrack(deviceId) {
|
||||
this._disposeAudioInputPreview()
|
||||
return this._disposeAudioInputPreview()
|
||||
.then(() => createLocalTrack('audio', deviceId))
|
||||
.then(jitsiLocalTrack => {
|
||||
this.setState({
|
||||
@@ -234,7 +246,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_createVideoInputTrack(deviceId) {
|
||||
this._disposeVideoInputPreview()
|
||||
return this._disposeVideoInputPreview()
|
||||
.then(() => createLocalTrack('video', deviceId))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (!jitsiLocalTrack) {
|
||||
|
||||
@@ -5,14 +5,7 @@ import uuid from 'uuid';
|
||||
import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
|
||||
import { appNavigate, getName } from '../../app';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN,
|
||||
CONFERENCE_JOINED,
|
||||
SET_AUDIO_ONLY,
|
||||
getCurrentConference
|
||||
} from '../../base/conference';
|
||||
import { SET_AUDIO_ONLY } from '../../base/conference';
|
||||
import { getInviteURL } from '../../base/connection';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
@@ -20,6 +13,16 @@ import {
|
||||
setAudioMuted
|
||||
} from '../../base/media';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
import {
|
||||
SESSION_CONFIGURED,
|
||||
SESSION_ENDED,
|
||||
SESSION_FAILED,
|
||||
SESSION_STARTED,
|
||||
SET_SESSION,
|
||||
getCurrentSession,
|
||||
getSession,
|
||||
setSession
|
||||
} from '../../base/session';
|
||||
import {
|
||||
TRACK_ADDED,
|
||||
TRACK_REMOVED,
|
||||
@@ -51,21 +54,12 @@ CallKit && MiddlewareRegistry.register(store => next => action => {
|
||||
});
|
||||
break;
|
||||
|
||||
case CONFERENCE_FAILED:
|
||||
return _conferenceFailed(store, next, action);
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
return _conferenceJoined(store, next, action);
|
||||
|
||||
case CONFERENCE_LEFT:
|
||||
return _conferenceLeft(store, next, action);
|
||||
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
return _conferenceWillJoin(store, next, action);
|
||||
|
||||
case SET_AUDIO_ONLY:
|
||||
return _setAudioOnly(store, next, action);
|
||||
|
||||
case SET_SESSION:
|
||||
return _setSession(store, next, action);
|
||||
|
||||
case TRACK_ADDED:
|
||||
case TRACK_REMOVED:
|
||||
case TRACK_UPDATED:
|
||||
@@ -127,6 +121,35 @@ function _appWillMount({ dispatch, getState }, next, action) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Store}store - FIXME.
|
||||
* @param {Dispatch} next - FIXME.
|
||||
* @param {Action} action - FIXME.
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
* @private
|
||||
*/
|
||||
function _setSession(store, next, action) {
|
||||
const { state } = action.session;
|
||||
|
||||
switch (state) {
|
||||
case SESSION_CONFIGURED:
|
||||
return _sessionConfigured(store, next, action);
|
||||
|
||||
case SESSION_ENDED:
|
||||
return _sessionEnded(store, next, action);
|
||||
|
||||
case SESSION_FAILED:
|
||||
return _sessionFailed(store, next, action);
|
||||
|
||||
case SESSION_STARTED:
|
||||
return _sessionJoined(store, next, action);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature callkit that the action {@link CONFERENCE_FAILED} is
|
||||
* being dispatched within a specific redux {@code store}.
|
||||
@@ -140,21 +163,14 @@ function _appWillMount({ dispatch, getState }, next, action) {
|
||||
* @private
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceFailed(store, next, action) {
|
||||
const result = next(action);
|
||||
function _sessionFailed(store, next, action) {
|
||||
const callUUID = _getCallUUIDForSessionAction(store, action);
|
||||
|
||||
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
|
||||
// prevented the user from joining a specific conference but the app may be
|
||||
// able to eventually join the conference.
|
||||
if (!action.error.recoverable) {
|
||||
const { callUUID } = action.conference;
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.reportCallFailed(callUUID);
|
||||
}
|
||||
if (callUUID) {
|
||||
CallKit.reportCallFailed(callUUID);
|
||||
}
|
||||
|
||||
return result;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,16 +186,14 @@ function _conferenceFailed(store, next, action) {
|
||||
* @private
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceJoined(store, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const { callUUID } = action.conference;
|
||||
function _sessionJoined(store, next, action) {
|
||||
const callUUID = _getCallUUIDForSessionAction(store, action);
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.reportConnectedOutgoingCall(callUUID);
|
||||
}
|
||||
|
||||
return result;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,16 +209,34 @@ function _conferenceJoined(store, next, action) {
|
||||
* @private
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceLeft(store, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const { callUUID } = action.conference;
|
||||
function _sessionEnded(store, next, action) {
|
||||
const callUUID = _getCallUUIDForSessionAction(store, action);
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.endCall(callUUID);
|
||||
}
|
||||
|
||||
return result;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Store} store - FIXME.
|
||||
* @param {Object} action - FIXME.
|
||||
* @returns {string|undefined}
|
||||
* @private
|
||||
*/
|
||||
function _getCallUUIDForSessionAction(store, action) {
|
||||
const url = action.session.url;
|
||||
const session = getSession(store, url);
|
||||
const callUUID = session && session.callkit && session.callkit.callUUID;
|
||||
|
||||
if (!callUUID) {
|
||||
console.info(`CALLKIT SESSION NOT FOUND FOR URL: ${url}`);
|
||||
}
|
||||
|
||||
return callUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,27 +252,32 @@ function _conferenceLeft(store, next, action) {
|
||||
* @private
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceWillJoin({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const { conference } = action;
|
||||
function _sessionConfigured({ getState }, next, action) {
|
||||
const state = getState();
|
||||
const { callHandle, callUUID } = state['features/base/config'];
|
||||
const { callHandle, callUUID: _callUUID } = state['features/base/config'];
|
||||
const url = getInviteURL(state);
|
||||
const handle = callHandle || url.toString();
|
||||
const hasVideo = !isVideoMutedByAudioOnly(state);
|
||||
|
||||
// When assigning the call UUID, do so in upper case, since iOS will return
|
||||
// it upper cased.
|
||||
conference.callUUID = (callUUID || uuid.v4()).toUpperCase();
|
||||
const callUUID = (_callUUID || uuid.v4()).toUpperCase();
|
||||
|
||||
CallKit.startCall(conference.callUUID, handle, hasVideo)
|
||||
// Store the callUUID in the session
|
||||
action.session.callkit = {
|
||||
callUUID
|
||||
};
|
||||
|
||||
CallKit.startCall(callUUID, handle, hasVideo)
|
||||
.then(() => {
|
||||
const session = getSession(getState(), action.session.url);
|
||||
const { callee } = state['features/base/jwt'];
|
||||
const displayName
|
||||
= state['features/base/config'].callDisplayName
|
||||
|| (callee && callee.name)
|
||||
|| state['features/base/conference'].room;
|
||||
|| (session && session.room);
|
||||
|
||||
console.info(`CALLKIT WILL USE NAME: ${displayName}`);
|
||||
|
||||
const muted
|
||||
= isLocalTrackMuted(
|
||||
@@ -248,11 +285,11 @@ function _conferenceWillJoin({ getState }, next, action) {
|
||||
MEDIA_TYPE.AUDIO);
|
||||
|
||||
// eslint-disable-next-line object-property-newline
|
||||
CallKit.updateCall(conference.callUUID, { displayName, hasVideo });
|
||||
CallKit.setMuted(conference.callUUID, muted);
|
||||
CallKit.updateCall(callUUID, { displayName, hasVideo });
|
||||
CallKit.setMuted(callUUID, muted);
|
||||
});
|
||||
|
||||
return result;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,18 +300,47 @@ function _conferenceWillJoin({ getState }, next, action) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onPerformEndCallAction({ callUUID }) {
|
||||
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
|
||||
const conference = getCurrentConference(getState);
|
||||
const { dispatch } = this; // eslint-disable-line no-invalid-this
|
||||
// eslint-disable-next-line max-len
|
||||
const session = _findSessionForCallUUID(this, callUUID); // eslint-disable-line no-invalid-this
|
||||
|
||||
if (conference && conference.callUUID === callUUID) {
|
||||
if (session) {
|
||||
// We arrive here when a call is ended by the system, for example, when
|
||||
// another incoming call is received and the user selects "End &
|
||||
// Accept".
|
||||
delete conference.callUUID;
|
||||
dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
callkit: undefined
|
||||
}));
|
||||
dispatch(appNavigate(undefined));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Store} getState - FIXME.
|
||||
* @param {string} callUUID - FIXME.
|
||||
* @returns {Object|null}
|
||||
* @private
|
||||
*/
|
||||
function _findSessionForCallUUID({ getState }, callUUID) {
|
||||
const sessions = getState()['features/base/session'];
|
||||
|
||||
for (const session of sessions.values()) {
|
||||
const _callUUID = session.callkit && session.callkit.callUUID;
|
||||
|
||||
if (callUUID === _callUUID) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
console.info(`SESSION NOT FOUND FOR CALL ID: ${callUUID}`);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles CallKit's event {@code performSetMutedCallAction}.
|
||||
*
|
||||
@@ -283,10 +349,11 @@ function _onPerformEndCallAction({ callUUID }) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onPerformSetMutedCallAction({ callUUID, muted }) {
|
||||
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
|
||||
const conference = getCurrentConference(getState);
|
||||
const { dispatch } = this; // eslint-disable-line no-invalid-this
|
||||
// eslint-disable-next-line max-len
|
||||
const session = _findSessionForCallUUID(this, callUUID); // eslint-disable-line no-invalid-this
|
||||
|
||||
if (conference && conference.callUUID === callUUID) {
|
||||
if (session) {
|
||||
muted = Boolean(muted); // eslint-disable-line no-param-reassign
|
||||
sendAnalytics(createTrackMutedEvent('audio', 'callkit', muted));
|
||||
dispatch(setAudioMuted(muted, /* ensureTrack */ true));
|
||||
@@ -316,11 +383,11 @@ function _onPerformSetMutedCallAction({ callUUID, muted }) {
|
||||
function _setAudioOnly({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const session = getCurrentSession(state);
|
||||
|
||||
if (conference && conference.callUUID) {
|
||||
if (session && session.callUUID) {
|
||||
CallKit.updateCall(
|
||||
conference.callUUID,
|
||||
session.callUUID,
|
||||
{ hasVideo: !action.audioOnly });
|
||||
}
|
||||
|
||||
@@ -369,20 +436,25 @@ function _syncTrackState({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
const { jitsiTrack } = action.track;
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (jitsiTrack.isLocal() && conference && conference.callUUID) {
|
||||
// It could go over all sessions here, but even if we'd support simultaneous
|
||||
// sessions / putting on hold, probably only the active session would be
|
||||
// holding the tracks.
|
||||
const session = getCurrentSession(state);
|
||||
const callUUID = session && session.callkit && session.callkit.callUUID;
|
||||
|
||||
if (jitsiTrack.isLocal() && callUUID) {
|
||||
switch (jitsiTrack.getType()) {
|
||||
case 'audio': {
|
||||
const tracks = state['features/base/tracks'];
|
||||
const muted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
|
||||
|
||||
CallKit.setMuted(conference.callUUID, muted);
|
||||
CallKit.setMuted(callUUID, muted);
|
||||
break;
|
||||
}
|
||||
case 'video': {
|
||||
CallKit.updateCall(
|
||||
conference.callUUID,
|
||||
callUUID,
|
||||
{ hasVideo: !isVideoMutedByAudioOnly(state) });
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,56 @@
|
||||
// @flow
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import { getAppProp } from '../../app';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
SET_ROOM,
|
||||
forEachConference,
|
||||
isRoomValid
|
||||
CONFERENCE_WILL_LEAVE
|
||||
} from '../../base/conference';
|
||||
import { LOAD_CONFIG_ERROR } from '../../base/config';
|
||||
import { CONNECTION_FAILED } from '../../base/connection';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
import { getSymbolDescription, toURLString } from '../../base/util';
|
||||
import {
|
||||
SESSION_ENDED,
|
||||
SESSION_FAILED,
|
||||
SESSION_STARTED,
|
||||
SESSION_WILL_END,
|
||||
SESSION_WILL_START,
|
||||
SET_SESSION
|
||||
} from '../../base/session';
|
||||
import { getSymbolDescription } from '../../base/util';
|
||||
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Symbol} state - FIXME.
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
function _stateToApiEventName(state) {
|
||||
switch (state) {
|
||||
case SESSION_WILL_START:
|
||||
return getSymbolDescription(CONFERENCE_WILL_JOIN);
|
||||
|
||||
case SESSION_STARTED:
|
||||
return getSymbolDescription(CONFERENCE_JOINED);
|
||||
|
||||
case SESSION_WILL_END:
|
||||
return getSymbolDescription(CONFERENCE_WILL_LEAVE);
|
||||
|
||||
case SESSION_ENDED:
|
||||
return getSymbolDescription(CONFERENCE_LEFT);
|
||||
|
||||
case SESSION_FAILED:
|
||||
return getSymbolDescription(CONFERENCE_FAILED);
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
import { sendEvent } from './functions';
|
||||
|
||||
/**
|
||||
@@ -27,63 +61,19 @@ import { sendEvent } from './functions';
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
action.type && console.info(`ACTION ${getSymbolDescription(action.type)}`);
|
||||
|
||||
const result = next(action);
|
||||
const { type } = action;
|
||||
|
||||
switch (type) {
|
||||
case CONFERENCE_FAILED: {
|
||||
const { error, ...data } = action;
|
||||
|
||||
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
|
||||
// prevented the user from joining a specific conference but the app may
|
||||
// be able to eventually join the conference. For example, the app will
|
||||
// ask the user for a password upon
|
||||
// JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
|
||||
// conference afterwards. Such errors are to not reach the native
|
||||
// counterpart of the External API (or at least not in the
|
||||
// fatality/finality semantics attributed to
|
||||
// conferenceFailed:/onConferenceFailed).
|
||||
if (!error.recoverable) {
|
||||
_sendConferenceEvent(store, /* action */ {
|
||||
error: _toErrorString(error),
|
||||
...data
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
case CONFERENCE_LEFT:
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
_sendConferenceEvent(store, action);
|
||||
break;
|
||||
|
||||
case CONNECTION_FAILED:
|
||||
!action.error.recoverable
|
||||
&& _sendConferenceFailedOnConnectionError(store, action);
|
||||
case SET_SESSION:
|
||||
_setSession(store, action);
|
||||
break;
|
||||
|
||||
case ENTER_PICTURE_IN_PICTURE:
|
||||
sendEvent(store, getSymbolDescription(type), /* data */ {});
|
||||
break;
|
||||
|
||||
case LOAD_CONFIG_ERROR: {
|
||||
const { error, locationURL } = action;
|
||||
|
||||
sendEvent(
|
||||
store,
|
||||
getSymbolDescription(type),
|
||||
/* data */ {
|
||||
error: _toErrorString(error),
|
||||
url: toURLString(locationURL)
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_ROOM:
|
||||
_maybeTriggerEarlyConferenceWillJoin(store, action);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -110,145 +100,47 @@ function _toErrorString(
|
||||
}
|
||||
|
||||
/**
|
||||
* If {@link SET_ROOM} action happens for a valid conference room this method
|
||||
* will emit an early {@link CONFERENCE_WILL_JOIN} event to let the external API
|
||||
* know that a conference is being joined. Before that happens a connection must
|
||||
* be created and only then base/conference feature would emit
|
||||
* {@link CONFERENCE_WILL_JOIN}. That is fine for the Jitsi Meet app, because
|
||||
* that's the a conference instance gets created, but it's too late for
|
||||
* the external API to learn that. The latter {@link CONFERENCE_WILL_JOIN} is
|
||||
* swallowed in {@link _swallowEvent}.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Action} action - The redux action.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeTriggerEarlyConferenceWillJoin(store, action) {
|
||||
const { locationURL } = store.getState()['features/base/connection'];
|
||||
const { room } = action;
|
||||
|
||||
isRoomValid(room) && locationURL && sendEvent(
|
||||
store,
|
||||
getSymbolDescription(CONFERENCE_WILL_JOIN),
|
||||
/* data */ {
|
||||
url: toURLString(locationURL)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an event to the native counterpart of the External API for a specific
|
||||
* conference-related redux action.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Action} action - The redux action.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _sendConferenceEvent(
|
||||
store: Object,
|
||||
action: {
|
||||
conference: Object,
|
||||
type: Symbol,
|
||||
url: ?string
|
||||
}) {
|
||||
const { conference, type, ...data } = action;
|
||||
|
||||
// For these (redux) actions, conference identifies a JitsiConference
|
||||
// instance. The external API cannot transport such an object so we have to
|
||||
// transport an "equivalent".
|
||||
if (conference) {
|
||||
data.url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
|
||||
}
|
||||
|
||||
_swallowEvent(store, action, data)
|
||||
|| sendEvent(store, getSymbolDescription(type), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends {@link CONFERENCE_FAILED} event when the {@link CONNECTION_FAILED}
|
||||
* occurs. It should be done only if the connection fails before the conference
|
||||
* instance is created. Otherwise the eventual failure event is supposed to be
|
||||
* emitted by the base/conference feature.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Action} action - The redux action.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _sendConferenceFailedOnConnectionError(store, action) {
|
||||
const { locationURL } = store.getState()['features/base/connection'];
|
||||
const { connection } = action;
|
||||
|
||||
locationURL
|
||||
&& forEachConference(
|
||||
store,
|
||||
|
||||
// If there's any conference in the base/conference state then the
|
||||
// base/conference feature is supposed to emit a failure.
|
||||
conference => conference.getConnection() !== connection)
|
||||
&& sendEvent(
|
||||
store,
|
||||
getSymbolDescription(CONFERENCE_FAILED),
|
||||
/* data */ {
|
||||
url: toURLString(locationURL),
|
||||
error: action.error.name
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether to not send a {@code CONFERENCE_LEFT} event to the native
|
||||
* counterpart of the External API.
|
||||
* Sends a specific event to the native counterpart of the External API. Native
|
||||
* apps may listen to such events via the mechanisms provided by the (native)
|
||||
* mobile Jitsi Meet SDK.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @param {Action} action - The redux action which is causing the sending of the
|
||||
* event.
|
||||
* @param {string} name - The name of the event to send.
|
||||
* @param {Object} data - The details/specifics of the event to send determined
|
||||
* by/associated with the specified {@code action}.
|
||||
* @returns {boolean} If the specified event is to not be sent, {@code true};
|
||||
* otherwise, {@code false}.
|
||||
* by/associated with the specified {@code name}.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _swallowConferenceLeft({ getState }, action, { url }) {
|
||||
// XXX Internally, we work with JitsiConference instances. Externally
|
||||
// though, we deal with URL strings. The relation between the two is many to
|
||||
// one so it's technically and practically possible (by externally loading
|
||||
// the same URL string multiple times) to try to send CONFERENCE_LEFT
|
||||
// externally for a URL string which identifies a JitsiConference that the
|
||||
// app is internally legitimately working with.
|
||||
let swallowConferenceLeft = false;
|
||||
function _sendEvent(store: Object, name: string, data: Object) {
|
||||
// The JavaScript App needs to provide uniquely identifying information to
|
||||
// the native ExternalAPI module so that the latter may match the former to
|
||||
// the native JitsiMeetView which hosts it.
|
||||
const externalAPIScope = getAppProp(store, 'externalAPIScope');
|
||||
|
||||
url
|
||||
&& forEachConference(getState, (conference, conferenceURL) => {
|
||||
if (conferenceURL && conferenceURL.toString() === url) {
|
||||
swallowConferenceLeft = true;
|
||||
}
|
||||
console.info(
|
||||
`EXT EVENT ${name} URL: ${data.url} DATA: ${JSON.stringify(data)}`);
|
||||
|
||||
return !swallowConferenceLeft;
|
||||
});
|
||||
|
||||
return swallowConferenceLeft;
|
||||
externalAPIScope
|
||||
&& NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether to not send a specific event to the native counterpart of
|
||||
* the External API.
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @param {Action} action - The redux action which is causing the sending of the
|
||||
* event.
|
||||
* @param {Object} data - The details/specifics of the event to send determined
|
||||
* by/associated with the specified {@code action}.
|
||||
* @returns {boolean} If the specified event is to not be sent, {@code true};
|
||||
* otherwise, {@code false}.
|
||||
* @param {Store} store - FIXME.
|
||||
* @param {Action} action - FIXME.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function _swallowEvent(store, action, data) {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_LEFT:
|
||||
return _swallowConferenceLeft(store, action, data);
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
// CONFERENCE_WILL_JOIN is dispatched to the external API on SET_ROOM,
|
||||
// before the connection is created, so we need to swallow the original
|
||||
// one emitted by base/conference.
|
||||
return true;
|
||||
function _setSession(store, action) {
|
||||
const { error, state, url } = action.session;
|
||||
const apiEventName = _stateToApiEventName(state);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
apiEventName && _sendEvent(
|
||||
store,
|
||||
apiEventName,
|
||||
/* data */ {
|
||||
url,
|
||||
error: error && _toErrorString(error)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
SESSION_FAILED,
|
||||
getCurrentSession,
|
||||
setSession
|
||||
} from '../base/session';
|
||||
|
||||
import {
|
||||
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
|
||||
SET_FATAL_ERROR,
|
||||
@@ -57,3 +63,37 @@ export function setFatalError(fatalError) {
|
||||
fatalError
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME naming is not quite accurate - came from the previous method which was
|
||||
* reemitting the action. I feel that this part needs more discussion. Changing
|
||||
* it back to emitting the original action which caused the fatal error will
|
||||
* also require changes to how it's being detected (currently through the state
|
||||
* listener, but we'd have to go back to the middleware way).
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function reemitFatalError() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { fatalError } = state['features/overlay'];
|
||||
|
||||
if (fatalError) {
|
||||
const session = getCurrentSession(state);
|
||||
|
||||
if (session) {
|
||||
dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
state: SESSION_FAILED,
|
||||
error: fatalError
|
||||
}));
|
||||
} else {
|
||||
console.info('No current session!');
|
||||
}
|
||||
dispatch(setFatalError(undefined));
|
||||
} else {
|
||||
console.info('NO FATAL ERROR');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { LoadingIndicator } from '../../base/react';
|
||||
|
||||
import AbstractPageReloadOverlay, { abstractMapStateToProps }
|
||||
from './AbstractPageReloadOverlay';
|
||||
import { setFatalError } from '../actions';
|
||||
import { reemitFatalError } from '../actions';
|
||||
import OverlayFrame from './OverlayFrame';
|
||||
import { pageReloadOverlay as styles } from './styles';
|
||||
|
||||
@@ -41,7 +41,7 @@ class PageReloadOverlay extends AbstractPageReloadOverlay {
|
||||
*/
|
||||
_onCancel() {
|
||||
clearInterval(this._interval);
|
||||
this.props.dispatch(setFatalError(undefined));
|
||||
this.props.dispatch(reemitFatalError());
|
||||
this.props.dispatch(appNavigate(undefined));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getAvailableDevices } from '../../../base/devices';
|
||||
import { DialogWithTabs, hideDialog } from '../../../base/dialog';
|
||||
import {
|
||||
DeviceSelection,
|
||||
@@ -77,6 +78,9 @@ class SettingsDialog extends Component<Props> {
|
||||
const tabs = _tabs.map(tab => {
|
||||
return {
|
||||
...tab,
|
||||
onMount: tab.onMount
|
||||
? (...args) => dispatch(tab.onMount(...args))
|
||||
: undefined,
|
||||
submit: (...args) => dispatch(tab.submit(...args))
|
||||
};
|
||||
});
|
||||
@@ -133,7 +137,22 @@ function _mapStateToProps(state) {
|
||||
name: SETTINGS_TABS.DEVICES,
|
||||
component: DeviceSelection,
|
||||
label: 'settings.devices',
|
||||
onMount: getAvailableDevices,
|
||||
props: getDeviceSelectionDialogProps(state),
|
||||
propsUpdateFunction: (tabState, newProps) => {
|
||||
// Ensure the device selection tab gets updated when new devices
|
||||
// are found by taking the new props and only preserving the
|
||||
// current user selected devices. If this were not done, the
|
||||
// tab would keep using a copy of the initial props it received,
|
||||
// leaving the device list to become stale.
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
selectedAudioInputId: tabState.selectedAudioInputId,
|
||||
selectedAudioOutputId: tabState.selectedAudioOutputId,
|
||||
selectedVideoInputId: tabState.selectedVideoInputId
|
||||
};
|
||||
},
|
||||
styles: 'settings-pane devices-pane',
|
||||
submit: submitDeviceSelectionTab
|
||||
});
|
||||
|
||||
@@ -74,24 +74,30 @@ export function shouldShowOnlyDeviceSelection() {
|
||||
export function getMoreTabProps(stateful: Object | Function) {
|
||||
const state = toState(stateful);
|
||||
const language = i18next.language || DEFAULT_LANGUAGE;
|
||||
const conference = state['features/base/conference'];
|
||||
const {
|
||||
conference,
|
||||
followMeEnabled,
|
||||
startAudioMutedPolicy,
|
||||
startVideoMutedPolicy
|
||||
} = state['features/base/conference'];
|
||||
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
|
||||
// The settings sections to display.
|
||||
const showModeratorSettings
|
||||
= configuredTabs.includes('moderator')
|
||||
&& localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
|
||||
const showModeratorSettings = Boolean(
|
||||
conference
|
||||
&& configuredTabs.includes('moderator')
|
||||
&& localParticipant.role === PARTICIPANT_ROLE.MODERATOR);
|
||||
|
||||
return {
|
||||
currentLanguage: language,
|
||||
followMeEnabled: Boolean(conference.followMeEnabled),
|
||||
followMeEnabled: Boolean(conference && followMeEnabled),
|
||||
languages: LANGUAGES,
|
||||
showLanguageSettings: configuredTabs.includes('language'),
|
||||
showModeratorSettings,
|
||||
startAudioMuted: Boolean(conference.startAudioMutedPolicy),
|
||||
startVideoMuted: Boolean(conference.startVideoMutedPolicy)
|
||||
startAudioMuted: Boolean(conference && startAudioMutedPolicy),
|
||||
startVideoMuted: Boolean(conference && startVideoMutedPolicy)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,12 +112,16 @@ export function getMoreTabProps(stateful: Object | Function) {
|
||||
*/
|
||||
export function getProfileTabProps(stateful: Object | Function) {
|
||||
const state = toState(stateful);
|
||||
const conference = state['features/base/conference'];
|
||||
const {
|
||||
authEnabled,
|
||||
authLogin,
|
||||
conference
|
||||
} = state['features/base/conference'];
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
authEnabled: conference.authEnabled,
|
||||
authLogin: conference.authLogin,
|
||||
authEnabled: Boolean(conference && authEnabled),
|
||||
authLogin: Boolean(conference && authLogin),
|
||||
displayName: localParticipant.name,
|
||||
email: localParticipant.email
|
||||
};
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* The type of json-message which indicates that json carries a
|
||||
* transcription result.
|
||||
@@ -67,6 +65,12 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _endpointMessageReceived({ dispatch, getState }, next, action) {
|
||||
if (!(action.json
|
||||
&& (action.json.type === JSON_TYPE_TRANSCRIPTION_RESULT
|
||||
|| action.json.type === JSON_TYPE_TRANSLATION_RESULT))) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const json = action.json;
|
||||
const translationLanguage
|
||||
= getState()['features/base/conference'].conference
|
||||
@@ -85,16 +89,15 @@ function _endpointMessageReceived({ dispatch, getState }, next, action) {
|
||||
|
||||
const newTranscriptMessage = {
|
||||
participantName,
|
||||
final: json.text
|
||||
final: json.text,
|
||||
clearTimeOut: undefined
|
||||
};
|
||||
|
||||
setClearerOnTranscriptMessage(dispatch,
|
||||
transcriptMessageID, newTranscriptMessage);
|
||||
dispatch(updateTranscriptMessage(transcriptMessageID,
|
||||
newTranscriptMessage));
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(removeTranscriptMessage(transcriptMessageID));
|
||||
}, REMOVE_AFTER_MS);
|
||||
|
||||
} else if (json.type === JSON_TYPE_TRANSCRIPTION_RESULT
|
||||
&& !translationLanguage) {
|
||||
// Displays interim and final results without any translation if
|
||||
@@ -109,6 +112,9 @@ function _endpointMessageReceived({ dispatch, getState }, next, action) {
|
||||
= { ...getState()['features/subtitles'].transcriptMessages
|
||||
.get(transcriptMessageID) || { participantName } };
|
||||
|
||||
setClearerOnTranscriptMessage(dispatch,
|
||||
transcriptMessageID, newTranscriptMessage);
|
||||
|
||||
// If this is final result, update the state as a final result
|
||||
// and start a count down to remove the subtitle from the state
|
||||
if (!isInterim) {
|
||||
@@ -116,10 +122,6 @@ function _endpointMessageReceived({ dispatch, getState }, next, action) {
|
||||
newTranscriptMessage.final = text;
|
||||
dispatch(updateTranscriptMessage(transcriptMessageID,
|
||||
newTranscriptMessage));
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(removeTranscriptMessage(transcriptMessageID));
|
||||
}, REMOVE_AFTER_MS);
|
||||
} else if (stability > 0.85) {
|
||||
|
||||
// If the message has a high stability, we can update the
|
||||
@@ -146,3 +148,26 @@ function _endpointMessageReceived({ dispatch, getState }, next, action) {
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a timeout on a TranscriptMessage object so it clears itself when it's not
|
||||
* updated.
|
||||
*
|
||||
* @param {Function} dispatch - Dispatch remove action to store.
|
||||
* @param {string} transcriptMessageID - The id of the message to remove.
|
||||
* @param {Object} transcriptMessage - The message to remove.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function setClearerOnTranscriptMessage(
|
||||
dispatch,
|
||||
transcriptMessageID,
|
||||
transcriptMessage) {
|
||||
if (transcriptMessage.clearTimeOut) {
|
||||
clearTimeout(transcriptMessage.clearTimeOut);
|
||||
}
|
||||
|
||||
transcriptMessage.clearTimeOut = setTimeout(() => {
|
||||
dispatch(removeTranscriptMessage(transcriptMessageID));
|
||||
}, REMOVE_AFTER_MS);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { DialogContainer } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { Watermarks } from '../../base/react';
|
||||
import { RecentList } from '../../recent-list';
|
||||
import { openSettingsDialog } from '../../settings';
|
||||
|
||||
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
|
||||
|
||||
@@ -18,6 +20,15 @@ import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
|
||||
* @extends AbstractWelcomePage
|
||||
*/
|
||||
class WelcomePage extends AbstractWelcomePage {
|
||||
/**
|
||||
* Default values for {@code WelcomePage} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
_room: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new WelcomePage instance.
|
||||
*
|
||||
@@ -55,6 +66,7 @@ class WelcomePage extends AbstractWelcomePage {
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onFormSubmit = this._onFormSubmit.bind(this);
|
||||
this._onOpenSettings = this._onOpenSettings.bind(this);
|
||||
this._onRoomChange = this._onRoomChange.bind(this);
|
||||
this._setAdditionalContentRef
|
||||
= this._setAdditionalContentRef.bind(this);
|
||||
@@ -155,6 +167,9 @@ class WelcomePage extends AbstractWelcomePage {
|
||||
ref = { this._setAdditionalContentRef } />
|
||||
: null }
|
||||
</div>
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
<DialogContainer />
|
||||
</AtlasKitThemeProvider>
|
||||
</AtlasKitThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -172,6 +187,16 @@ class WelcomePage extends AbstractWelcomePage {
|
||||
this._onJoin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@code SettingsDialog}.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenSettings() {
|
||||
this.props.dispatch(openSettingsDialog());
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the super to account for the differences in the argument types
|
||||
* provided by HTML and React Native text inputs.
|
||||
|
||||
@@ -63,7 +63,6 @@ export default {
|
||||
LOGOUT: 'UI.logout',
|
||||
VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
|
||||
AUDIO_DEVICE_CHANGED: 'UI.audio_device_changed',
|
||||
AUDIO_OUTPUT_DEVICE_CHANGED: 'UI.audio_output_device_changed',
|
||||
|
||||
/**
|
||||
* Notifies interested listeners that the follow-me feature is enabled or
|
||||
|
||||
Reference in New Issue
Block a user