2018-05-10 04:45:24 +00:00
|
|
|
// @flow
|
2017-06-07 15:50:30 +00:00
|
|
|
|
2021-01-21 12:07:00 +00:00
|
|
|
import debounce from 'lodash/debounce';
|
2021-01-13 13:48:29 +00:00
|
|
|
import { NativeEventEmitter, NativeModules } from 'react-native';
|
|
|
|
|
2021-01-20 12:06:45 +00:00
|
|
|
import { ENDPOINT_TEXT_MESSAGE_NAME } from '../../../../modules/API/constants';
|
2021-01-13 13:48:29 +00:00
|
|
|
import { appNavigate } from '../../app/actions';
|
2021-05-27 13:53:20 +00:00
|
|
|
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app/actionTypes';
|
2017-06-07 15:50:30 +00:00
|
|
|
import {
|
|
|
|
CONFERENCE_FAILED,
|
|
|
|
CONFERENCE_JOINED,
|
|
|
|
CONFERENCE_LEFT,
|
|
|
|
CONFERENCE_WILL_JOIN,
|
2018-04-25 23:07:25 +00:00
|
|
|
JITSI_CONFERENCE_URL_KEY,
|
|
|
|
SET_ROOM,
|
fix(base/participants): ensure default local id outside of conference
Makes sure that whenever a conference is left or switched, the local
participant's id will be equal to the default value.
The problem fixed by this commit is a situation where the local
participant may end up sharing the same ID with it's "ghost" when
rejoining a disconnected conference. The most important and easiest to
hit case is when the conference is left after the CONFERENCE_FAILED
event.
Another rare and harder to encounter in the real world issue is
where CONFERENCE_LEFT may come with the delay due to it's asynchronous
nature. The step by step scenario is as follows: trying to leave a
conference, but the network is not doing well, so it takes time,
requests are timing out. After getting back to the welcome page the
the CONFERENCE_LEFT has not arrived yet. The same conference is joined
again and the load config may timeout, but it will be read from the
cache. Now the network gets better and conference is joining which
results in our ghost participant added to the redux state. At this point
there's the root issue: two participants with the same id, because the
local one was neither cleared nor set to the new one yet
(PARTICIPANT_JOINED come, before CONFERENCE_JOINED where we adjust the
id). Then comes CONFERENCE_JOINED and we try to update our local id.
We're updating the ID of both ghost and local participant. It could be
also that the delayed CONFERENCE_LEFT comes for the old conference, but
it's too late and it would update the id for both participants.
The approach here reasons that the ID of the local participant
may be reset as soon as the local participant and, respectively, her ID
is no longer involved in a recoverable JitsiConference of interest to
the user and, consequently, the app.
Co-authored-by: Pawel Domas <pawel.domas@jitsi.org>
Co-authored-by: Lyubo Marinov <lmarinov@atlassian.com>
2018-05-16 20:08:34 +00:00
|
|
|
forEachConference,
|
2021-01-20 12:06:45 +00:00
|
|
|
getCurrentConference,
|
2018-04-25 23:07:25 +00:00
|
|
|
isRoomValid
|
2017-06-07 15:50:30 +00:00
|
|
|
} from '../../base/conference';
|
2017-08-25 15:21:01 +00:00
|
|
|
import { LOAD_CONFIG_ERROR } from '../../base/config';
|
2019-04-25 08:14:23 +00:00
|
|
|
import {
|
|
|
|
CONNECTION_DISCONNECTED,
|
|
|
|
CONNECTION_FAILED,
|
|
|
|
JITSI_CONNECTION_CONFERENCE_KEY,
|
2019-11-14 12:33:20 +00:00
|
|
|
JITSI_CONNECTION_URL_KEY,
|
|
|
|
getURLWithoutParams
|
2019-04-25 08:14:23 +00:00
|
|
|
} from '../../base/connection';
|
2021-01-20 12:06:45 +00:00
|
|
|
import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
|
2021-02-24 21:45:07 +00:00
|
|
|
import { MEDIA_TYPE } from '../../base/media';
|
2021-03-23 13:30:17 +00:00
|
|
|
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
|
2021-07-09 12:36:19 +00:00
|
|
|
import {
|
|
|
|
PARTICIPANT_JOINED,
|
|
|
|
PARTICIPANT_LEFT,
|
|
|
|
getParticipantById,
|
|
|
|
getRemoteParticipants,
|
|
|
|
getLocalParticipant
|
|
|
|
} from '../../base/participants';
|
2021-01-21 12:07:00 +00:00
|
|
|
import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
|
|
|
|
import { toggleScreensharing } from '../../base/tracks';
|
2021-02-17 14:26:40 +00:00
|
|
|
import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
|
|
|
|
import { openChat } from '../../chat/actions';
|
|
|
|
import { sendMessage, setPrivateMessageRecipient, closeChat } from '../../chat/actions.any';
|
2021-01-21 20:46:47 +00:00
|
|
|
import { muteLocal } from '../../video-menu/actions';
|
2018-02-19 22:52:21 +00:00
|
|
|
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
|
2018-02-01 16:02:07 +00:00
|
|
|
|
2021-01-21 12:07:00 +00:00
|
|
|
import { setParticipantsWithScreenShare } from './actions';
|
2018-07-15 14:50:54 +00:00
|
|
|
import { sendEvent } from './functions';
|
2021-01-20 12:06:45 +00:00
|
|
|
import logger from './logger';
|
2018-07-15 14:50:54 +00:00
|
|
|
|
2021-02-17 14:26:40 +00:00
|
|
|
/**
|
|
|
|
* Event which will be emitted on the native side when a chat message is received
|
|
|
|
* through the channel.
|
|
|
|
*/
|
|
|
|
const CHAT_MESSAGE_RECEIVED = 'CHAT_MESSAGE_RECEIVED';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Event which will be emitted on the native side when the chat dialog is displayed/closed.
|
|
|
|
*/
|
|
|
|
const CHAT_TOGGLED = 'CHAT_TOGGLED';
|
|
|
|
|
2019-03-07 12:07:48 +00:00
|
|
|
/**
|
|
|
|
* Event which will be emitted on the native side to indicate the conference
|
|
|
|
* has ended either by user request or because an error was produced.
|
|
|
|
*/
|
|
|
|
const CONFERENCE_TERMINATED = 'CONFERENCE_TERMINATED';
|
|
|
|
|
2021-01-20 12:06:45 +00:00
|
|
|
/**
|
|
|
|
* Event which will be emitted on the native side to indicate a message was received
|
|
|
|
* through the channel.
|
|
|
|
*/
|
|
|
|
const ENDPOINT_TEXT_MESSAGE_RECEIVED = 'ENDPOINT_TEXT_MESSAGE_RECEIVED';
|
|
|
|
|
2021-01-21 12:07:00 +00:00
|
|
|
/**
|
|
|
|
* Event which will be emitted on the native side to indicate a participant togggles
|
|
|
|
* the screen share.
|
|
|
|
*/
|
|
|
|
const SCREEN_SHARE_TOGGLED = 'SCREEN_SHARE_TOGGLED';
|
|
|
|
|
2021-02-04 12:26:35 +00:00
|
|
|
/**
|
|
|
|
* Event which will be emitted on the native side with the participant info array.
|
|
|
|
*/
|
|
|
|
const PARTICIPANTS_INFO_RETRIEVED = 'PARTICIPANTS_INFO_RETRIEVED';
|
|
|
|
|
2021-01-13 13:48:29 +00:00
|
|
|
const { ExternalAPI } = NativeModules;
|
|
|
|
const eventEmitter = new NativeEventEmitter(ExternalAPI);
|
|
|
|
|
2017-06-07 15:50:30 +00:00
|
|
|
/**
|
|
|
|
* Middleware that captures Redux actions and uses the ExternalAPI module to
|
|
|
|
* turn them into native events so the application knows about them.
|
|
|
|
*
|
|
|
|
* @param {Store} store - Redux store.
|
|
|
|
* @returns {Function}
|
|
|
|
*/
|
|
|
|
MiddlewareRegistry.register(store => next => action => {
|
2017-06-09 04:59:31 +00:00
|
|
|
const result = next(action);
|
2018-05-23 21:10:04 +00:00
|
|
|
const { type } = action;
|
2017-06-07 15:50:30 +00:00
|
|
|
|
2018-05-23 21:10:04 +00:00
|
|
|
switch (type) {
|
2021-01-13 13:48:29 +00:00
|
|
|
case APP_WILL_MOUNT:
|
2021-01-20 12:06:45 +00:00
|
|
|
_registerForNativeEvents(store);
|
2021-01-13 13:48:29 +00:00
|
|
|
break;
|
2021-05-27 13:53:20 +00:00
|
|
|
case APP_WILL_UNMOUNT:
|
|
|
|
_unregisterForNativeEvents();
|
|
|
|
break;
|
2017-10-05 15:21:37 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2017-06-09 04:59:31 +00:00
|
|
|
case CONFERENCE_LEFT:
|
|
|
|
case CONFERENCE_WILL_JOIN:
|
2017-10-05 15:21:37 +00:00
|
|
|
_sendConferenceEvent(store, action);
|
2017-06-07 15:50:30 +00:00
|
|
|
break;
|
2017-08-25 15:21:01 +00:00
|
|
|
|
2021-01-20 12:06:45 +00:00
|
|
|
case CONFERENCE_JOINED:
|
|
|
|
_sendConferenceEvent(store, action);
|
|
|
|
_registerForEndpointTextMessages(store);
|
|
|
|
break;
|
|
|
|
|
2019-04-25 08:14:23 +00:00
|
|
|
case CONNECTION_DISCONNECTED: {
|
|
|
|
// FIXME: This is a hack. See the description in the JITSI_CONNECTION_CONFERENCE_KEY constant definition.
|
|
|
|
// Check if this connection was attached to any conference. If it wasn't, fake a CONFERENCE_TERMINATED event.
|
|
|
|
const { connection } = action;
|
|
|
|
const conference = connection[JITSI_CONNECTION_CONFERENCE_KEY];
|
|
|
|
|
|
|
|
if (!conference) {
|
|
|
|
// This action will arrive late, so the locationURL stored on the state is no longer valid.
|
|
|
|
const locationURL = connection[JITSI_CONNECTION_URL_KEY];
|
|
|
|
|
|
|
|
sendEvent(
|
|
|
|
store,
|
|
|
|
CONFERENCE_TERMINATED,
|
|
|
|
/* data */ {
|
2019-11-14 12:33:20 +00:00
|
|
|
url: _normalizeUrl(locationURL)
|
2019-04-25 08:14:23 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2018-04-25 23:10:13 +00:00
|
|
|
case CONNECTION_FAILED:
|
2018-05-01 19:58:46 +00:00
|
|
|
!action.error.recoverable
|
|
|
|
&& _sendConferenceFailedOnConnectionError(store, action);
|
2018-04-25 23:10:13 +00:00
|
|
|
break;
|
|
|
|
|
2018-02-19 22:52:21 +00:00
|
|
|
case ENTER_PICTURE_IN_PICTURE:
|
2019-03-19 12:34:05 +00:00
|
|
|
sendEvent(store, type, /* data */ {});
|
2018-02-19 22:52:21 +00:00
|
|
|
break;
|
|
|
|
|
2017-08-25 15:21:01 +00:00
|
|
|
case LOAD_CONFIG_ERROR: {
|
2018-05-23 21:10:04 +00:00
|
|
|
const { error, locationURL } = action;
|
2017-08-25 15:21:01 +00:00
|
|
|
|
2018-07-15 14:50:54 +00:00
|
|
|
sendEvent(
|
2018-05-23 21:10:04 +00:00
|
|
|
store,
|
2019-03-07 12:07:48 +00:00
|
|
|
CONFERENCE_TERMINATED,
|
2018-05-23 21:10:04 +00:00
|
|
|
/* data */ {
|
|
|
|
error: _toErrorString(error),
|
2019-11-14 12:33:20 +00:00
|
|
|
url: _normalizeUrl(locationURL)
|
2018-05-23 21:10:04 +00:00
|
|
|
});
|
2017-08-25 15:21:01 +00:00
|
|
|
break;
|
|
|
|
}
|
2018-04-25 23:07:25 +00:00
|
|
|
|
2021-02-17 14:26:40 +00:00
|
|
|
case OPEN_CHAT:
|
|
|
|
case CLOSE_CHAT: {
|
|
|
|
sendEvent(
|
|
|
|
store,
|
|
|
|
CHAT_TOGGLED,
|
|
|
|
/* data */ {
|
|
|
|
isOpen: action.type === OPEN_CHAT
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-01-13 13:48:29 +00:00
|
|
|
case PARTICIPANT_JOINED:
|
|
|
|
case PARTICIPANT_LEFT: {
|
|
|
|
const { participant } = action;
|
|
|
|
|
|
|
|
sendEvent(
|
|
|
|
store,
|
|
|
|
action.type,
|
|
|
|
/* data */ {
|
|
|
|
isLocal: participant.local,
|
|
|
|
email: participant.email,
|
|
|
|
name: participant.name,
|
2021-02-04 12:26:35 +00:00
|
|
|
participantId: participant.id,
|
|
|
|
displayName: participant.displayName,
|
|
|
|
avatarUrl: participant.avatarURL,
|
|
|
|
role: participant.role
|
2021-01-13 13:48:29 +00:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2018-04-25 23:07:25 +00:00
|
|
|
case SET_ROOM:
|
|
|
|
_maybeTriggerEarlyConferenceWillJoin(store, action);
|
|
|
|
break;
|
2021-01-13 13:48:29 +00:00
|
|
|
|
|
|
|
case SET_AUDIO_MUTED:
|
|
|
|
sendEvent(
|
|
|
|
store,
|
|
|
|
'AUDIO_MUTED_CHANGED',
|
|
|
|
/* data */ {
|
|
|
|
muted: action.muted
|
|
|
|
});
|
|
|
|
break;
|
2021-03-23 13:30:17 +00:00
|
|
|
|
|
|
|
case SET_VIDEO_MUTED:
|
|
|
|
sendEvent(
|
|
|
|
store,
|
|
|
|
'VIDEO_MUTED_CHANGED',
|
|
|
|
/* data */ {
|
|
|
|
muted: action.muted
|
|
|
|
});
|
|
|
|
break;
|
2017-06-07 15:50:30 +00:00
|
|
|
}
|
|
|
|
|
2017-06-09 04:59:31 +00:00
|
|
|
return result;
|
2017-06-07 15:50:30 +00:00
|
|
|
});
|
|
|
|
|
2021-01-21 12:07:00 +00:00
|
|
|
/**
|
|
|
|
* Listen for changes to the known media tracks and look
|
|
|
|
* for updates to screen shares for emitting native events.
|
|
|
|
* The listener is debounced to avoid state thrashing that might occur,
|
|
|
|
* especially when switching in or out of p2p.
|
|
|
|
*/
|
|
|
|
StateListenerRegistry.register(
|
|
|
|
/* selector */ state => state['features/base/tracks'],
|
|
|
|
/* listener */ debounce((tracks, store) => {
|
|
|
|
const oldScreenShares = store.getState()['features/mobile/external-api'].screenShares || [];
|
|
|
|
const newScreenShares = tracks
|
|
|
|
.filter(track => track.mediaType === 'video' && track.videoType === 'desktop')
|
|
|
|
.map(track => track.participantId);
|
|
|
|
|
|
|
|
oldScreenShares.forEach(participantId => {
|
|
|
|
if (!newScreenShares.includes(participantId)) {
|
|
|
|
sendEvent(
|
|
|
|
store,
|
|
|
|
SCREEN_SHARE_TOGGLED,
|
|
|
|
/* data */ {
|
|
|
|
participantId,
|
|
|
|
sharing: false
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
newScreenShares.forEach(participantId => {
|
|
|
|
if (!oldScreenShares.includes(participantId)) {
|
|
|
|
sendEvent(
|
|
|
|
store,
|
|
|
|
SCREEN_SHARE_TOGGLED,
|
|
|
|
/* data */ {
|
|
|
|
participantId,
|
|
|
|
sharing: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
store.dispatch(setParticipantsWithScreenShare(newScreenShares));
|
|
|
|
|
|
|
|
}, 100));
|
|
|
|
|
2021-07-09 12:36:19 +00:00
|
|
|
/**
|
|
|
|
* Returns a participant info object based on the passed participant object from redux.
|
|
|
|
*
|
|
|
|
* @param {Participant} participant - The participant object from the redux store.
|
|
|
|
* @returns {Object} - The participant info object.
|
|
|
|
*/
|
|
|
|
function _participantToParticipantInfo(participant) {
|
|
|
|
return {
|
|
|
|
isLocal: participant.local,
|
|
|
|
email: participant.email,
|
|
|
|
name: participant.name,
|
|
|
|
participantId: participant.id,
|
|
|
|
displayName: participant.displayName,
|
|
|
|
avatarUrl: participant.avatarURL,
|
|
|
|
role: participant.role
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-01-13 13:48:29 +00:00
|
|
|
/**
|
|
|
|
* Registers for events sent from the native side via NativeEventEmitter.
|
|
|
|
*
|
2021-01-20 12:06:45 +00:00
|
|
|
* @param {Store} store - The redux store.
|
2021-01-13 13:48:29 +00:00
|
|
|
* @private
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2021-02-17 14:26:40 +00:00
|
|
|
function _registerForNativeEvents(store) {
|
|
|
|
const { getState, dispatch } = store;
|
|
|
|
|
2021-01-13 13:48:29 +00:00
|
|
|
eventEmitter.addListener(ExternalAPI.HANG_UP, () => {
|
|
|
|
dispatch(appNavigate(undefined));
|
|
|
|
});
|
|
|
|
|
|
|
|
eventEmitter.addListener(ExternalAPI.SET_AUDIO_MUTED, ({ muted }) => {
|
2021-03-23 13:30:17 +00:00
|
|
|
dispatch(muteLocal(muted, MEDIA_TYPE.AUDIO));
|
|
|
|
});
|
|
|
|
|
|
|
|
eventEmitter.addListener(ExternalAPI.SET_VIDEO_MUTED, ({ muted }) => {
|
|
|
|
dispatch(muteLocal(muted, MEDIA_TYPE.VIDEO));
|
2021-01-13 13:48:29 +00:00
|
|
|
});
|
2021-01-20 12:06:45 +00:00
|
|
|
|
|
|
|
eventEmitter.addListener(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE, ({ to, message }) => {
|
|
|
|
const conference = getCurrentConference(getState());
|
|
|
|
|
|
|
|
try {
|
|
|
|
conference && conference.sendEndpointMessage(to, {
|
|
|
|
name: ENDPOINT_TEXT_MESSAGE_NAME,
|
|
|
|
text: message
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
logger.warn('Cannot send endpointMessage', error);
|
|
|
|
}
|
|
|
|
});
|
2021-01-21 12:07:00 +00:00
|
|
|
|
2021-04-22 12:24:17 +00:00
|
|
|
eventEmitter.addListener(ExternalAPI.TOGGLE_SCREEN_SHARE, ({ enabled }) => {
|
|
|
|
dispatch(toggleScreensharing(enabled));
|
2021-01-21 12:07:00 +00:00
|
|
|
});
|
2021-02-04 12:26:35 +00:00
|
|
|
|
|
|
|
eventEmitter.addListener(ExternalAPI.RETRIEVE_PARTICIPANTS_INFO, ({ requestId }) => {
|
|
|
|
|
2021-07-09 12:36:19 +00:00
|
|
|
const participantsInfo = [];
|
|
|
|
const remoteParticipants = getRemoteParticipants(store);
|
|
|
|
const localParticipant = getLocalParticipant(store);
|
|
|
|
|
|
|
|
participantsInfo.push(_participantToParticipantInfo(localParticipant));
|
|
|
|
remoteParticipants.forEach(participant => {
|
|
|
|
if (!participant.isFakeParticipant) {
|
|
|
|
participantsInfo.push(_participantToParticipantInfo(participant));
|
|
|
|
}
|
2021-02-04 12:26:35 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
sendEvent(
|
|
|
|
store,
|
|
|
|
PARTICIPANTS_INFO_RETRIEVED,
|
|
|
|
/* data */ {
|
|
|
|
participantsInfo,
|
|
|
|
requestId
|
|
|
|
});
|
|
|
|
});
|
2021-02-17 14:26:40 +00:00
|
|
|
|
|
|
|
eventEmitter.addListener(ExternalAPI.OPEN_CHAT, ({ to }) => {
|
|
|
|
const participant = getParticipantById(store, to);
|
|
|
|
|
|
|
|
dispatch(openChat(participant));
|
|
|
|
});
|
|
|
|
|
|
|
|
eventEmitter.addListener(ExternalAPI.CLOSE_CHAT, () => {
|
|
|
|
dispatch(closeChat());
|
|
|
|
});
|
|
|
|
|
|
|
|
eventEmitter.addListener(ExternalAPI.SEND_CHAT_MESSAGE, ({ message, to }) => {
|
|
|
|
const participant = getParticipantById(store, to);
|
|
|
|
|
|
|
|
if (participant) {
|
|
|
|
dispatch(setPrivateMessageRecipient(participant));
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatch(sendMessage(message));
|
|
|
|
});
|
|
|
|
|
2021-01-20 12:06:45 +00:00
|
|
|
}
|
|
|
|
|
2021-05-27 13:53:20 +00:00
|
|
|
/**
|
|
|
|
* Unregister for events sent from the native side via NativeEventEmitter.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function _unregisterForNativeEvents() {
|
|
|
|
eventEmitter.removeAllListeners(ExternalAPI.HANG_UP);
|
|
|
|
eventEmitter.removeAllListeners(ExternalAPI.SET_AUDIO_MUTED);
|
|
|
|
eventEmitter.removeAllListeners(ExternalAPI.SET_VIDEO_MUTED);
|
|
|
|
eventEmitter.removeAllListeners(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE);
|
|
|
|
eventEmitter.removeAllListeners(ExternalAPI.TOGGLE_SCREEN_SHARE);
|
|
|
|
eventEmitter.removeAllListeners(ExternalAPI.RETRIEVE_PARTICIPANTS_INFO);
|
|
|
|
eventEmitter.removeAllListeners(ExternalAPI.OPEN_CHAT);
|
|
|
|
eventEmitter.removeAllListeners(ExternalAPI.CLOSE_CHAT);
|
|
|
|
eventEmitter.removeAllListeners(ExternalAPI.SEND_CHAT_MESSAGE);
|
|
|
|
}
|
|
|
|
|
2021-01-20 12:06:45 +00:00
|
|
|
/**
|
|
|
|
* Registers for endpoint messages sent on conference data channel.
|
|
|
|
*
|
|
|
|
* @param {Store} store - The redux store.
|
|
|
|
* @private
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function _registerForEndpointTextMessages(store) {
|
|
|
|
const conference = getCurrentConference(store.getState());
|
|
|
|
|
|
|
|
conference && conference.on(
|
|
|
|
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
|
|
|
(...args) => {
|
|
|
|
if (args && args.length >= 2) {
|
|
|
|
const [ sender, eventData ] = args;
|
|
|
|
|
|
|
|
if (eventData.name === ENDPOINT_TEXT_MESSAGE_NAME) {
|
|
|
|
sendEvent(
|
|
|
|
store,
|
|
|
|
ENDPOINT_TEXT_MESSAGE_RECEIVED,
|
|
|
|
/* data */ {
|
|
|
|
message: eventData.text,
|
|
|
|
senderId: sender._id
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2021-02-17 14:26:40 +00:00
|
|
|
|
|
|
|
conference.on(
|
|
|
|
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
|
|
|
(id, message, timestamp) => {
|
|
|
|
sendEvent(
|
|
|
|
store,
|
|
|
|
CHAT_MESSAGE_RECEIVED,
|
|
|
|
/* data */ {
|
|
|
|
senderId: id,
|
|
|
|
message,
|
|
|
|
isPrivate: false,
|
|
|
|
timestamp
|
|
|
|
});
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
conference.on(
|
|
|
|
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
|
|
|
|
(id, message, timestamp) => {
|
|
|
|
sendEvent(
|
|
|
|
store,
|
|
|
|
CHAT_MESSAGE_RECEIVED,
|
|
|
|
/* data */ {
|
|
|
|
senderId: id,
|
|
|
|
message,
|
|
|
|
isPrivate: true,
|
|
|
|
timestamp
|
|
|
|
});
|
|
|
|
}
|
|
|
|
);
|
2021-01-13 13:48:29 +00:00
|
|
|
}
|
|
|
|
|
2017-10-05 15:21:37 +00:00
|
|
|
/**
|
|
|
|
* Returns a {@code String} representation of a specific error {@code Object}.
|
|
|
|
*
|
|
|
|
* @param {Error|Object|string} error - The error {@code Object} to return a
|
|
|
|
* {@code String} representation of.
|
|
|
|
* @returns {string} A {@code String} representation of the specified
|
|
|
|
* {@code error}.
|
|
|
|
*/
|
|
|
|
function _toErrorString(
|
|
|
|
error: Error | { message: ?string, name: ?string } | string) {
|
|
|
|
// XXX In lib-jitsi-meet and jitsi-meet we utilize errors in the form of
|
|
|
|
// strings, Error instances, and plain objects which resemble Error.
|
|
|
|
return (
|
|
|
|
error
|
|
|
|
? typeof error === 'string'
|
|
|
|
? error
|
2017-10-26 14:52:57 +00:00
|
|
|
: Error.prototype.toString.apply(error)
|
2017-10-05 15:21:37 +00:00
|
|
|
: '');
|
|
|
|
}
|
|
|
|
|
2018-04-25 23:07:25 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
2018-07-15 14:50:54 +00:00
|
|
|
isRoomValid(room) && locationURL && sendEvent(
|
2018-04-25 23:07:25 +00:00
|
|
|
store,
|
2019-03-19 12:34:05 +00:00
|
|
|
CONFERENCE_WILL_JOIN,
|
2018-04-25 23:07:25 +00:00
|
|
|
/* data */ {
|
2019-11-14 12:33:20 +00:00
|
|
|
url: _normalizeUrl(locationURL)
|
2018-04-25 23:07:25 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-11-14 12:33:20 +00:00
|
|
|
/**
|
|
|
|
* Normalizes the given URL for presentation over the external API.
|
|
|
|
*
|
|
|
|
* @param {URL} url -The URL to normalize.
|
|
|
|
* @returns {string} - The normalized URL as a string.
|
|
|
|
*/
|
|
|
|
function _normalizeUrl(url: URL) {
|
|
|
|
return getURLWithoutParams(url).href;
|
|
|
|
}
|
|
|
|
|
2017-10-05 15:21:37 +00:00
|
|
|
/**
|
|
|
|
* 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,
|
2018-03-05 01:25:23 +00:00
|
|
|
action: {
|
2017-12-01 21:07:58 +00:00
|
|
|
conference: Object,
|
2019-03-19 12:34:05 +00:00
|
|
|
type: string,
|
2017-12-01 21:07:58 +00:00
|
|
|
url: ?string
|
|
|
|
}) {
|
2018-03-05 01:25:23 +00:00
|
|
|
const { conference, type, ...data } = action;
|
|
|
|
|
2017-10-05 15:21:37 +00:00
|
|
|
// 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) {
|
2019-11-14 12:33:20 +00:00
|
|
|
data.url = _normalizeUrl(conference[JITSI_CONFERENCE_URL_KEY]);
|
2017-10-05 15:21:37 +00:00
|
|
|
}
|
|
|
|
|
2019-03-07 12:07:48 +00:00
|
|
|
if (_swallowEvent(store, action, data)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let type_;
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case CONFERENCE_FAILED:
|
|
|
|
case CONFERENCE_LEFT:
|
|
|
|
type_ = CONFERENCE_TERMINATED;
|
|
|
|
break;
|
|
|
|
default:
|
2019-03-19 12:34:05 +00:00
|
|
|
type_ = type;
|
2019-03-07 12:07:48 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
sendEvent(store, type_, data);
|
2017-09-06 03:40:12 +00:00
|
|
|
}
|
|
|
|
|
2018-04-25 23:10:13 +00:00
|
|
|
/**
|
2019-03-07 12:07:48 +00:00
|
|
|
* Sends {@link CONFERENCE_TERMINATED} event when the {@link CONNECTION_FAILED}
|
2018-06-07 10:13:06 +00:00
|
|
|
* 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.
|
2018-04-25 23:10:13 +00:00
|
|
|
*
|
|
|
|
* @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'];
|
2018-06-07 10:13:06 +00:00
|
|
|
const { connection } = action;
|
2018-04-25 23:10:13 +00:00
|
|
|
|
2018-06-07 10:13:06 +00:00
|
|
|
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)
|
2018-07-15 14:50:54 +00:00
|
|
|
&& sendEvent(
|
2018-04-25 23:10:13 +00:00
|
|
|
store,
|
2019-03-07 12:07:48 +00:00
|
|
|
CONFERENCE_TERMINATED,
|
2018-04-25 23:10:13 +00:00
|
|
|
/* data */ {
|
2019-11-14 12:33:20 +00:00
|
|
|
url: _normalizeUrl(locationURL),
|
2018-04-25 23:10:13 +00:00
|
|
|
error: action.error.name
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-03-05 01:25:23 +00:00
|
|
|
/**
|
|
|
|
* Determines whether to not send a {@code CONFERENCE_LEFT} event to the native
|
|
|
|
* counterpart of the External API.
|
|
|
|
*
|
|
|
|
* @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}.
|
|
|
|
*/
|
|
|
|
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.
|
fix(base/participants): ensure default local id outside of conference
Makes sure that whenever a conference is left or switched, the local
participant's id will be equal to the default value.
The problem fixed by this commit is a situation where the local
participant may end up sharing the same ID with it's "ghost" when
rejoining a disconnected conference. The most important and easiest to
hit case is when the conference is left after the CONFERENCE_FAILED
event.
Another rare and harder to encounter in the real world issue is
where CONFERENCE_LEFT may come with the delay due to it's asynchronous
nature. The step by step scenario is as follows: trying to leave a
conference, but the network is not doing well, so it takes time,
requests are timing out. After getting back to the welcome page the
the CONFERENCE_LEFT has not arrived yet. The same conference is joined
again and the load config may timeout, but it will be read from the
cache. Now the network gets better and conference is joining which
results in our ghost participant added to the redux state. At this point
there's the root issue: two participants with the same id, because the
local one was neither cleared nor set to the new one yet
(PARTICIPANT_JOINED come, before CONFERENCE_JOINED where we adjust the
id). Then comes CONFERENCE_JOINED and we try to update our local id.
We're updating the ID of both ghost and local participant. It could be
also that the delayed CONFERENCE_LEFT comes for the old conference, but
it's too late and it would update the id for both participants.
The approach here reasons that the ID of the local participant
may be reset as soon as the local participant and, respectively, her ID
is no longer involved in a recoverable JitsiConference of interest to
the user and, consequently, the app.
Co-authored-by: Pawel Domas <pawel.domas@jitsi.org>
Co-authored-by: Lyubo Marinov <lmarinov@atlassian.com>
2018-05-16 20:08:34 +00:00
|
|
|
let swallowConferenceLeft = false;
|
2018-03-05 01:25:23 +00:00
|
|
|
|
fix(base/participants): ensure default local id outside of conference
Makes sure that whenever a conference is left or switched, the local
participant's id will be equal to the default value.
The problem fixed by this commit is a situation where the local
participant may end up sharing the same ID with it's "ghost" when
rejoining a disconnected conference. The most important and easiest to
hit case is when the conference is left after the CONFERENCE_FAILED
event.
Another rare and harder to encounter in the real world issue is
where CONFERENCE_LEFT may come with the delay due to it's asynchronous
nature. The step by step scenario is as follows: trying to leave a
conference, but the network is not doing well, so it takes time,
requests are timing out. After getting back to the welcome page the
the CONFERENCE_LEFT has not arrived yet. The same conference is joined
again and the load config may timeout, but it will be read from the
cache. Now the network gets better and conference is joining which
results in our ghost participant added to the redux state. At this point
there's the root issue: two participants with the same id, because the
local one was neither cleared nor set to the new one yet
(PARTICIPANT_JOINED come, before CONFERENCE_JOINED where we adjust the
id). Then comes CONFERENCE_JOINED and we try to update our local id.
We're updating the ID of both ghost and local participant. It could be
also that the delayed CONFERENCE_LEFT comes for the old conference, but
it's too late and it would update the id for both participants.
The approach here reasons that the ID of the local participant
may be reset as soon as the local participant and, respectively, her ID
is no longer involved in a recoverable JitsiConference of interest to
the user and, consequently, the app.
Co-authored-by: Pawel Domas <pawel.domas@jitsi.org>
Co-authored-by: Lyubo Marinov <lmarinov@atlassian.com>
2018-05-16 20:08:34 +00:00
|
|
|
url
|
|
|
|
&& forEachConference(getState, (conference, conferenceURL) => {
|
|
|
|
if (conferenceURL && conferenceURL.toString() === url) {
|
|
|
|
swallowConferenceLeft = true;
|
2018-03-05 01:25:23 +00:00
|
|
|
}
|
|
|
|
|
fix(base/participants): ensure default local id outside of conference
Makes sure that whenever a conference is left or switched, the local
participant's id will be equal to the default value.
The problem fixed by this commit is a situation where the local
participant may end up sharing the same ID with it's "ghost" when
rejoining a disconnected conference. The most important and easiest to
hit case is when the conference is left after the CONFERENCE_FAILED
event.
Another rare and harder to encounter in the real world issue is
where CONFERENCE_LEFT may come with the delay due to it's asynchronous
nature. The step by step scenario is as follows: trying to leave a
conference, but the network is not doing well, so it takes time,
requests are timing out. After getting back to the welcome page the
the CONFERENCE_LEFT has not arrived yet. The same conference is joined
again and the load config may timeout, but it will be read from the
cache. Now the network gets better and conference is joining which
results in our ghost participant added to the redux state. At this point
there's the root issue: two participants with the same id, because the
local one was neither cleared nor set to the new one yet
(PARTICIPANT_JOINED come, before CONFERENCE_JOINED where we adjust the
id). Then comes CONFERENCE_JOINED and we try to update our local id.
We're updating the ID of both ghost and local participant. It could be
also that the delayed CONFERENCE_LEFT comes for the old conference, but
it's too late and it would update the id for both participants.
The approach here reasons that the ID of the local participant
may be reset as soon as the local participant and, respectively, her ID
is no longer involved in a recoverable JitsiConference of interest to
the user and, consequently, the app.
Co-authored-by: Pawel Domas <pawel.domas@jitsi.org>
Co-authored-by: Lyubo Marinov <lmarinov@atlassian.com>
2018-05-16 20:08:34 +00:00
|
|
|
return !swallowConferenceLeft;
|
|
|
|
});
|
|
|
|
|
|
|
|
return swallowConferenceLeft;
|
2018-03-05 01:25:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether to not send a specific event to the native counterpart of
|
|
|
|
* the External API.
|
|
|
|
*
|
|
|
|
* @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}.
|
|
|
|
*/
|
|
|
|
function _swallowEvent(store, action, data) {
|
|
|
|
switch (action.type) {
|
|
|
|
case CONFERENCE_LEFT:
|
|
|
|
return _swallowConferenceLeft(store, action, data);
|
2018-04-25 23:07:25 +00:00
|
|
|
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;
|
2018-03-05 01:25:23 +00:00
|
|
|
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|