You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

middleware.ts 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. /* eslint-disable lines-around-comment */
  2. import { debounce } from 'lodash-es';
  3. import { NativeEventEmitter, NativeModules } from 'react-native';
  4. import { AnyAction } from 'redux';
  5. // @ts-ignore
  6. import { ENDPOINT_TEXT_MESSAGE_NAME } from '../../../../modules/API/constants';
  7. import { appNavigate } from '../../app/actions.native';
  8. import { IStore } from '../../app/types';
  9. import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app/actionTypes';
  10. import {
  11. CONFERENCE_BLURRED,
  12. CONFERENCE_FAILED,
  13. CONFERENCE_FOCUSED,
  14. CONFERENCE_JOINED,
  15. CONFERENCE_LEFT,
  16. CONFERENCE_WILL_JOIN,
  17. ENDPOINT_MESSAGE_RECEIVED,
  18. SET_ROOM
  19. } from '../../base/conference/actionTypes';
  20. import { JITSI_CONFERENCE_URL_KEY } from '../../base/conference/constants';
  21. import {
  22. forEachConference,
  23. getCurrentConference,
  24. isRoomValid
  25. } from '../../base/conference/functions';
  26. import { IJitsiConference } from '../../base/conference/reducer';
  27. import { CONNECTION_DISCONNECTED } from '../../base/connection/actionTypes';
  28. import {
  29. JITSI_CONNECTION_CONFERENCE_KEY,
  30. JITSI_CONNECTION_URL_KEY
  31. } from '../../base/connection/constants';
  32. import { getURLWithoutParams } from '../../base/connection/utils';
  33. import { JitsiConferenceEvents, JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
  34. import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
  35. import { toggleCameraFacingMode } from '../../base/media/actions';
  36. import { MEDIA_TYPE, VIDEO_TYPE } from '../../base/media/constants';
  37. import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../../base/participants/actionTypes';
  38. import {
  39. getLocalParticipant,
  40. getParticipantById,
  41. getRemoteParticipants,
  42. isScreenShareParticipantById
  43. } from '../../base/participants/functions';
  44. import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
  45. import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
  46. import { toggleScreensharing } from '../../base/tracks/actions.native';
  47. import { getLocalTracks, isLocalTrackMuted } from '../../base/tracks/functions.native';
  48. import { ITrack } from '../../base/tracks/types';
  49. import { CLOSE_CHAT, OPEN_CHAT } from '../../chat/actionTypes';
  50. import { closeChat, openChat, sendMessage, setPrivateMessageRecipient } from '../../chat/actions.native';
  51. import { isEnabled as isDropboxEnabled } from '../../dropbox/functions.native';
  52. import { hideNotification, showNotification } from '../../notifications/actions';
  53. import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../../notifications/constants';
  54. import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../recording/constants';
  55. import { getActiveSession } from '../../recording/functions';
  56. import { setRequestingSubtitles } from '../../subtitles/actions.any';
  57. import { CUSTOM_BUTTON_PRESSED } from '../../toolbox/actionTypes';
  58. import { muteLocal } from '../../video-menu/actions.native';
  59. import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture/actionTypes';
  60. // @ts-ignore
  61. import { isExternalAPIAvailable } from '../react-native-sdk/functions';
  62. import { READY_TO_CLOSE } from './actionTypes';
  63. import { setParticipantsWithScreenShare } from './actions';
  64. import { participantToParticipantInfo, sendEvent } from './functions';
  65. import logger from './logger';
  66. /**
  67. * Event which will be emitted on the native side when a chat message is received
  68. * through the channel.
  69. */
  70. const CHAT_MESSAGE_RECEIVED = 'CHAT_MESSAGE_RECEIVED';
  71. /**
  72. * Event which will be emitted on the native side when the chat dialog is displayed/closed.
  73. */
  74. const CHAT_TOGGLED = 'CHAT_TOGGLED';
  75. /**
  76. * Event which will be emitted on the native side to indicate the conference
  77. * has ended either by user request or because an error was produced.
  78. */
  79. const CONFERENCE_TERMINATED = 'CONFERENCE_TERMINATED';
  80. /**
  81. * Event which will be emitted on the native side to indicate a message was received
  82. * through the channel.
  83. */
  84. const ENDPOINT_TEXT_MESSAGE_RECEIVED = 'ENDPOINT_TEXT_MESSAGE_RECEIVED';
  85. /**
  86. * Event which will be emitted on the native side to indicate a participant toggles
  87. * the screen share.
  88. */
  89. const SCREEN_SHARE_TOGGLED = 'SCREEN_SHARE_TOGGLED';
  90. /**
  91. * Event which will be emitted on the native side with the participant info array.
  92. */
  93. const PARTICIPANTS_INFO_RETRIEVED = 'PARTICIPANTS_INFO_RETRIEVED';
  94. const externalAPIEnabled = isExternalAPIAvailable();
  95. let eventEmitter: any;
  96. const { ExternalAPI } = NativeModules;
  97. if (externalAPIEnabled) {
  98. eventEmitter = new NativeEventEmitter(ExternalAPI);
  99. }
  100. /**
  101. * Middleware that captures Redux actions and uses the ExternalAPI module to
  102. * turn them into native events so the application knows about them.
  103. *
  104. * @param {Store} store - Redux store.
  105. * @returns {Function}
  106. */
  107. externalAPIEnabled && MiddlewareRegistry.register(store => next => action => {
  108. const oldAudioMuted = store.getState()['features/base/media'].audio.muted;
  109. const result = next(action);
  110. const { type } = action;
  111. switch (type) {
  112. case APP_WILL_MOUNT:
  113. _registerForNativeEvents(store);
  114. break;
  115. case APP_WILL_UNMOUNT:
  116. _unregisterForNativeEvents();
  117. break;
  118. case CONFERENCE_FAILED: {
  119. const { error, ...data } = action;
  120. // XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
  121. // prevented the user from joining a specific conference but the app may
  122. // be able to eventually join the conference. For example, the app will
  123. // ask the user for a password upon
  124. // JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
  125. // conference afterwards. Such errors are to not reach the native
  126. // counterpart of the External API (or at least not in the
  127. // fatality/finality semantics attributed to
  128. // conferenceFailed:/onConferenceFailed).
  129. if (!error.recoverable) {
  130. _sendConferenceEvent(store, /* action */ {
  131. error: _toErrorString(error),
  132. ...data
  133. });
  134. }
  135. break;
  136. }
  137. case CONFERENCE_LEFT:
  138. _sendConferenceEvent(store, action);
  139. break;
  140. case CONFERENCE_JOINED:
  141. _sendConferenceEvent(store, action);
  142. _registerForEndpointTextMessages(store);
  143. break;
  144. case CONFERENCE_BLURRED:
  145. sendEvent(store, CONFERENCE_BLURRED, {});
  146. break;
  147. case CONFERENCE_FOCUSED:
  148. sendEvent(store, CONFERENCE_FOCUSED, {});
  149. break;
  150. case CONNECTION_DISCONNECTED: {
  151. // FIXME: This is a hack. See the description in the JITSI_CONNECTION_CONFERENCE_KEY constant definition.
  152. // Check if this connection was attached to any conference.
  153. // If it wasn't, fake a CONFERENCE_TERMINATED event.
  154. const { connection } = action;
  155. const conference = connection[JITSI_CONNECTION_CONFERENCE_KEY];
  156. if (!conference) {
  157. // This action will arrive late, so the locationURL stored on the state is no longer valid.
  158. const locationURL = connection[JITSI_CONNECTION_URL_KEY];
  159. sendEvent(
  160. store,
  161. CONFERENCE_TERMINATED,
  162. /* data */ {
  163. url: _normalizeUrl(locationURL)
  164. });
  165. }
  166. break;
  167. }
  168. case CUSTOM_BUTTON_PRESSED: {
  169. const { id, text } = action;
  170. sendEvent(
  171. store,
  172. CUSTOM_BUTTON_PRESSED,
  173. {
  174. id,
  175. text
  176. });
  177. break;
  178. }
  179. case ENDPOINT_MESSAGE_RECEIVED: {
  180. const { participant, data } = action;
  181. if (data?.name === ENDPOINT_TEXT_MESSAGE_NAME) {
  182. sendEvent(
  183. store,
  184. ENDPOINT_TEXT_MESSAGE_RECEIVED,
  185. /* data */ {
  186. message: data.text,
  187. senderId: participant.getId()
  188. });
  189. }
  190. break;
  191. }
  192. case ENTER_PICTURE_IN_PICTURE:
  193. sendEvent(store, type, /* data */ {});
  194. break;
  195. case OPEN_CHAT:
  196. case CLOSE_CHAT: {
  197. sendEvent(
  198. store,
  199. CHAT_TOGGLED,
  200. /* data */ {
  201. isOpen: action.type === OPEN_CHAT
  202. });
  203. break;
  204. }
  205. case PARTICIPANT_JOINED:
  206. case PARTICIPANT_LEFT: {
  207. // Skip these events while not in a conference. SDK users can still retrieve them.
  208. const { conference } = store.getState()['features/base/conference'];
  209. if (!conference) {
  210. break;
  211. }
  212. const { participant } = action;
  213. const isVirtualScreenshareParticipant = isScreenShareParticipantById(store.getState(), participant.id);
  214. if (isVirtualScreenshareParticipant) {
  215. break;
  216. }
  217. sendEvent(
  218. store,
  219. action.type,
  220. participantToParticipantInfo(participant) /* data */
  221. );
  222. break;
  223. }
  224. case READY_TO_CLOSE:
  225. sendEvent(store, type, /* data */ {});
  226. break;
  227. case SET_ROOM:
  228. _maybeTriggerEarlyConferenceWillJoin(store, action);
  229. break;
  230. case SET_AUDIO_MUTED:
  231. if (action.muted !== oldAudioMuted) {
  232. sendEvent(
  233. store,
  234. 'AUDIO_MUTED_CHANGED',
  235. /* data */ {
  236. muted: action.muted
  237. });
  238. }
  239. break;
  240. case SET_VIDEO_MUTED:
  241. sendEvent(
  242. store,
  243. 'VIDEO_MUTED_CHANGED',
  244. /* data */ {
  245. muted: action.muted
  246. });
  247. break;
  248. }
  249. return result;
  250. });
  251. /**
  252. * Listen for changes to the known media tracks and look
  253. * for updates to screen shares for emitting native events.
  254. * The listener is debounced to avoid state thrashing that might occur,
  255. * especially when switching in or out of p2p.
  256. */
  257. externalAPIEnabled && StateListenerRegistry.register(
  258. /* selector */ state => state['features/base/tracks'],
  259. /* listener */ debounce((tracks: ITrack[], store: IStore) => {
  260. const oldScreenShares = store.getState()['features/mobile/external-api'].screenShares || [];
  261. const newScreenShares = tracks
  262. .filter(track => track.mediaType === MEDIA_TYPE.SCREENSHARE || track.videoType === VIDEO_TYPE.DESKTOP)
  263. .map(track => track.participantId);
  264. oldScreenShares.forEach(participantId => {
  265. if (!newScreenShares.includes(participantId)) {
  266. sendEvent(
  267. store,
  268. SCREEN_SHARE_TOGGLED,
  269. /* data */ {
  270. participantId,
  271. sharing: false
  272. });
  273. }
  274. });
  275. newScreenShares.forEach(participantId => {
  276. if (!oldScreenShares.includes(participantId)) {
  277. sendEvent(
  278. store,
  279. SCREEN_SHARE_TOGGLED,
  280. /* data */ {
  281. participantId,
  282. sharing: true
  283. });
  284. }
  285. });
  286. store.dispatch(setParticipantsWithScreenShare(newScreenShares));
  287. }, 100));
  288. /**
  289. * Registers for events sent from the native side via NativeEventEmitter.
  290. *
  291. * @param {Store} store - The redux store.
  292. * @private
  293. * @returns {void}
  294. */
  295. function _registerForNativeEvents(store: IStore) {
  296. const { getState, dispatch } = store;
  297. eventEmitter.addListener(ExternalAPI.HANG_UP, () => {
  298. dispatch(appNavigate(undefined));
  299. });
  300. eventEmitter.addListener(ExternalAPI.SET_AUDIO_MUTED, ({ muted }: any) => {
  301. dispatch(muteLocal(muted, MEDIA_TYPE.AUDIO));
  302. });
  303. eventEmitter.addListener(ExternalAPI.SET_VIDEO_MUTED, ({ muted }: any) => {
  304. dispatch(muteLocal(muted, MEDIA_TYPE.VIDEO));
  305. });
  306. eventEmitter.addListener(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE, ({ to, message }: any) => {
  307. const conference = getCurrentConference(getState());
  308. try {
  309. conference?.sendEndpointMessage(to, {
  310. name: ENDPOINT_TEXT_MESSAGE_NAME,
  311. text: message
  312. });
  313. } catch (error) {
  314. logger.warn('Cannot send endpointMessage', error);
  315. }
  316. });
  317. eventEmitter.addListener(ExternalAPI.TOGGLE_SCREEN_SHARE, ({ enabled }: any) => {
  318. dispatch(toggleScreensharing(enabled));
  319. });
  320. eventEmitter.addListener(ExternalAPI.RETRIEVE_PARTICIPANTS_INFO, ({ requestId }: any) => {
  321. const participantsInfo = [];
  322. const remoteParticipants = getRemoteParticipants(store);
  323. const localParticipant = getLocalParticipant(store);
  324. localParticipant && participantsInfo.push(participantToParticipantInfo(localParticipant));
  325. remoteParticipants.forEach(participant => {
  326. if (!participant.fakeParticipant) {
  327. participantsInfo.push(participantToParticipantInfo(participant));
  328. }
  329. });
  330. sendEvent(
  331. store,
  332. PARTICIPANTS_INFO_RETRIEVED,
  333. /* data */ {
  334. participantsInfo,
  335. requestId
  336. });
  337. });
  338. eventEmitter.addListener(ExternalAPI.OPEN_CHAT, ({ to }: any) => {
  339. const participant = getParticipantById(store, to);
  340. dispatch(openChat(participant));
  341. });
  342. eventEmitter.addListener(ExternalAPI.CLOSE_CHAT, () => {
  343. dispatch(closeChat());
  344. });
  345. eventEmitter.addListener(ExternalAPI.SEND_CHAT_MESSAGE, ({ message, to }: any) => {
  346. const participant = getParticipantById(store, to);
  347. if (participant) {
  348. dispatch(setPrivateMessageRecipient(participant));
  349. }
  350. dispatch(sendMessage(message));
  351. });
  352. eventEmitter.addListener(ExternalAPI.SET_CLOSED_CAPTIONS_ENABLED,
  353. ({ enabled, displaySubtitles, language }: any) => {
  354. dispatch(setRequestingSubtitles(enabled, displaySubtitles, language));
  355. });
  356. eventEmitter.addListener(ExternalAPI.TOGGLE_CAMERA, () => {
  357. dispatch(toggleCameraFacingMode());
  358. });
  359. eventEmitter.addListener(ExternalAPI.SHOW_NOTIFICATION,
  360. ({ appearance, description, timeout, title, uid }: any) => {
  361. const validTypes = Object.values(NOTIFICATION_TYPE);
  362. const validTimeouts = Object.values(NOTIFICATION_TIMEOUT_TYPE);
  363. if (!validTypes.includes(appearance)) {
  364. logger.error(`Invalid notification type "${appearance}". Expecting one of ${validTypes}`);
  365. return;
  366. }
  367. if (!validTimeouts.includes(timeout)) {
  368. logger.error(`Invalid notification timeout "${timeout}". Expecting one of ${validTimeouts}`);
  369. return;
  370. }
  371. dispatch(showNotification({
  372. appearance,
  373. description,
  374. title,
  375. uid
  376. }, timeout));
  377. });
  378. eventEmitter.addListener(ExternalAPI.HIDE_NOTIFICATION, ({ uid }: any) => {
  379. dispatch(hideNotification(uid));
  380. });
  381. eventEmitter.addListener(ExternalAPI.START_RECORDING, (
  382. {
  383. mode,
  384. dropboxToken,
  385. shouldShare,
  386. rtmpStreamKey,
  387. rtmpBroadcastID,
  388. youtubeStreamKey,
  389. youtubeBroadcastID,
  390. extraMetadata = {},
  391. transcription
  392. }: any) => {
  393. const state = store.getState();
  394. const conference = getCurrentConference(state);
  395. if (!conference) {
  396. logger.error('Conference is not defined');
  397. return;
  398. }
  399. if (dropboxToken && !isDropboxEnabled(state)) {
  400. logger.error('Failed starting recording: dropbox is not enabled on this deployment');
  401. return;
  402. }
  403. if (mode === JitsiRecordingConstants.mode.STREAM && !(youtubeStreamKey || rtmpStreamKey)) {
  404. logger.error('Failed starting recording: missing youtube or RTMP stream key');
  405. return;
  406. }
  407. let recordingConfig;
  408. if (mode === JitsiRecordingConstants.mode.FILE) {
  409. const { recordingService } = state['features/base/config'];
  410. if (!recordingService?.enabled && !dropboxToken) {
  411. logger.error('Failed starting recording: the recording service is not enabled');
  412. return;
  413. }
  414. if (dropboxToken) {
  415. recordingConfig = {
  416. mode: JitsiRecordingConstants.mode.FILE,
  417. appData: JSON.stringify({
  418. 'file_recording_metadata': {
  419. ...extraMetadata,
  420. 'upload_credentials': {
  421. 'service_name': RECORDING_TYPES.DROPBOX,
  422. 'token': dropboxToken
  423. }
  424. }
  425. })
  426. };
  427. } else {
  428. recordingConfig = {
  429. mode: JitsiRecordingConstants.mode.FILE,
  430. appData: JSON.stringify({
  431. 'file_recording_metadata': {
  432. ...extraMetadata,
  433. 'share': shouldShare
  434. }
  435. })
  436. };
  437. }
  438. } else if (mode === JitsiRecordingConstants.mode.STREAM) {
  439. recordingConfig = {
  440. broadcastId: youtubeBroadcastID || rtmpBroadcastID,
  441. mode: JitsiRecordingConstants.mode.STREAM,
  442. streamId: youtubeStreamKey || rtmpStreamKey
  443. };
  444. }
  445. // Start audio / video recording, if requested.
  446. if (typeof recordingConfig !== 'undefined') {
  447. conference.startRecording(recordingConfig);
  448. }
  449. if (transcription) {
  450. store.dispatch(setRequestingSubtitles(true, false, null));
  451. conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
  452. isTranscribingEnabled: true
  453. });
  454. }
  455. });
  456. eventEmitter.addListener(ExternalAPI.STOP_RECORDING, ({ mode, transcription }: any) => {
  457. const state = store.getState();
  458. const conference = getCurrentConference(state);
  459. if (!conference) {
  460. logger.error('Conference is not defined');
  461. return;
  462. }
  463. if (transcription) {
  464. store.dispatch(setRequestingSubtitles(false, false, null));
  465. conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
  466. isTranscribingEnabled: false
  467. });
  468. }
  469. if (![ JitsiRecordingConstants.mode.FILE, JitsiRecordingConstants.mode.STREAM ].includes(mode)) {
  470. logger.error('Invalid recording mode provided!');
  471. return;
  472. }
  473. const activeSession = getActiveSession(state, mode);
  474. if (!activeSession?.id) {
  475. logger.error('No recording or streaming session found');
  476. return;
  477. }
  478. conference.stopRecording(activeSession.id);
  479. });
  480. }
  481. /**
  482. * Unregister for events sent from the native side via NativeEventEmitter.
  483. *
  484. * @private
  485. * @returns {void}
  486. */
  487. function _unregisterForNativeEvents() {
  488. eventEmitter.removeAllListeners(ExternalAPI.HANG_UP);
  489. eventEmitter.removeAllListeners(ExternalAPI.SET_AUDIO_MUTED);
  490. eventEmitter.removeAllListeners(ExternalAPI.SET_VIDEO_MUTED);
  491. eventEmitter.removeAllListeners(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE);
  492. eventEmitter.removeAllListeners(ExternalAPI.TOGGLE_SCREEN_SHARE);
  493. eventEmitter.removeAllListeners(ExternalAPI.RETRIEVE_PARTICIPANTS_INFO);
  494. eventEmitter.removeAllListeners(ExternalAPI.OPEN_CHAT);
  495. eventEmitter.removeAllListeners(ExternalAPI.CLOSE_CHAT);
  496. eventEmitter.removeAllListeners(ExternalAPI.SEND_CHAT_MESSAGE);
  497. eventEmitter.removeAllListeners(ExternalAPI.SET_CLOSED_CAPTIONS_ENABLED);
  498. eventEmitter.removeAllListeners(ExternalAPI.TOGGLE_CAMERA);
  499. eventEmitter.removeAllListeners(ExternalAPI.SHOW_NOTIFICATION);
  500. eventEmitter.removeAllListeners(ExternalAPI.HIDE_NOTIFICATION);
  501. eventEmitter.removeAllListeners(ExternalAPI.START_RECORDING);
  502. eventEmitter.removeAllListeners(ExternalAPI.STOP_RECORDING);
  503. }
  504. /**
  505. * Registers for endpoint messages sent on conference data channel.
  506. *
  507. * @param {Store} store - The redux store.
  508. * @private
  509. * @returns {void}
  510. */
  511. function _registerForEndpointTextMessages(store: IStore) {
  512. const conference = getCurrentConference(store.getState());
  513. conference?.on(
  514. JitsiConferenceEvents.MESSAGE_RECEIVED,
  515. (id: string, message: string, timestamp: number) => {
  516. sendEvent(
  517. store,
  518. CHAT_MESSAGE_RECEIVED,
  519. /* data */ {
  520. senderId: id,
  521. message,
  522. isPrivate: false,
  523. timestamp
  524. });
  525. }
  526. );
  527. conference?.on(
  528. JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
  529. (id: string, message: string, timestamp: number) => {
  530. sendEvent(
  531. store,
  532. CHAT_MESSAGE_RECEIVED,
  533. /* data */ {
  534. senderId: id,
  535. message,
  536. isPrivate: true,
  537. timestamp
  538. });
  539. }
  540. );
  541. }
  542. /**
  543. * Returns a {@code String} representation of a specific error {@code Object}.
  544. *
  545. * @param {Error|Object|string} error - The error {@code Object} to return a
  546. * {@code String} representation of.
  547. * @returns {string} A {@code String} representation of the specified
  548. * {@code error}.
  549. */
  550. function _toErrorString(
  551. error: Error | { message?: string; name?: string; } | string) {
  552. // XXX In lib-jitsi-meet and jitsi-meet we utilize errors in the form of
  553. // strings, Error instances, and plain objects which resemble Error.
  554. return (
  555. error
  556. ? typeof error === 'string'
  557. ? error
  558. : Error.prototype.toString.apply(error)
  559. : '');
  560. }
  561. /**
  562. * If {@link SET_ROOM} action happens for a valid conference room this method
  563. * will emit an early {@link CONFERENCE_WILL_JOIN} event to let the external API
  564. * know that a conference is being joined. Before that happens a connection must
  565. * be created and only then base/conference feature would emit
  566. * {@link CONFERENCE_WILL_JOIN}. That is fine for the Jitsi Meet app, because
  567. * that's the a conference instance gets created, but it's too late for
  568. * the external API to learn that. The latter {@link CONFERENCE_WILL_JOIN} is
  569. * swallowed in {@link _swallowEvent}.
  570. *
  571. * @param {Store} store - The redux store.
  572. * @param {Action} action - The redux action.
  573. * @returns {void}
  574. */
  575. function _maybeTriggerEarlyConferenceWillJoin(store: IStore, action: AnyAction) {
  576. const { locationURL } = store.getState()['features/base/connection'];
  577. const { room } = action;
  578. isRoomValid(room) && locationURL && sendEvent(
  579. store,
  580. CONFERENCE_WILL_JOIN,
  581. /* data */ {
  582. url: _normalizeUrl(locationURL)
  583. });
  584. }
  585. /**
  586. * Normalizes the given URL for presentation over the external API.
  587. *
  588. * @param {URL} url -The URL to normalize.
  589. * @returns {string} - The normalized URL as a string.
  590. */
  591. function _normalizeUrl(url: URL) {
  592. return getURLWithoutParams(url).href;
  593. }
  594. /**
  595. * Sends an event to the native counterpart of the External API for a specific
  596. * conference-related redux action.
  597. *
  598. * @param {Store} store - The redux store.
  599. * @param {Action} action - The redux action.
  600. * @returns {void}
  601. */
  602. function _sendConferenceEvent(
  603. store: IStore,
  604. action: {
  605. conference: IJitsiConference;
  606. isAudioMuted?: boolean;
  607. type: string;
  608. url?: string;
  609. }) {
  610. const { conference, type, ...data } = action;
  611. // For these (redux) actions, conference identifies a JitsiConference
  612. // instance. The external API cannot transport such an object so we have to
  613. // transport an "equivalent".
  614. if (conference) { // @ts-ignore
  615. data.url = _normalizeUrl(conference[JITSI_CONFERENCE_URL_KEY]);
  616. const localTracks = getLocalTracks(store.getState()['features/base/tracks']);
  617. const isAudioMuted = isLocalTrackMuted(localTracks, MEDIA_TYPE.AUDIO);
  618. data.isAudioMuted = isAudioMuted;
  619. }
  620. if (_swallowEvent(store, action, data)) {
  621. return;
  622. }
  623. let type_;
  624. switch (type) {
  625. case CONFERENCE_FAILED:
  626. case CONFERENCE_LEFT:
  627. type_ = CONFERENCE_TERMINATED;
  628. break;
  629. default:
  630. type_ = type;
  631. break;
  632. }
  633. sendEvent(store, type_, data);
  634. }
  635. /**
  636. * Determines whether to not send a {@code CONFERENCE_LEFT} event to the native
  637. * counterpart of the External API.
  638. *
  639. * @param {Object} store - The redux store.
  640. * @param {Action} action - The redux action which is causing the sending of the
  641. * event.
  642. * @param {Object} data - The details/specifics of the event to send determined
  643. * by/associated with the specified {@code action}.
  644. * @returns {boolean} If the specified event is to not be sent, {@code true};
  645. * otherwise, {@code false}.
  646. */
  647. function _swallowConferenceLeft({ getState }: IStore, action: AnyAction, { url }: { url: string; }) {
  648. // XXX Internally, we work with JitsiConference instances. Externally
  649. // though, we deal with URL strings. The relation between the two is many to
  650. // one so it's technically and practically possible (by externally loading
  651. // the same URL string multiple times) to try to send CONFERENCE_LEFT
  652. // externally for a URL string which identifies a JitsiConference that the
  653. // app is internally legitimately working with.
  654. let swallowConferenceLeft = false;
  655. url
  656. && forEachConference(getState, (conference, conferenceURL) => {
  657. if (conferenceURL && conferenceURL.toString() === url) {
  658. swallowConferenceLeft = true;
  659. }
  660. return !swallowConferenceLeft;
  661. });
  662. return swallowConferenceLeft;
  663. }
  664. /**
  665. * Determines whether to not send a specific event to the native counterpart of
  666. * the External API.
  667. *
  668. * @param {Object} store - The redux store.
  669. * @param {Action} action - The redux action which is causing the sending of the
  670. * event.
  671. * @param {Object} data - The details/specifics of the event to send determined
  672. * by/associated with the specified {@code action}.
  673. * @returns {boolean} If the specified event is to not be sent, {@code true};
  674. * otherwise, {@code false}.
  675. */
  676. function _swallowEvent(store: IStore, action: AnyAction, data: any) {
  677. switch (action.type) {
  678. case CONFERENCE_LEFT:
  679. return _swallowConferenceLeft(store, action, data);
  680. default:
  681. return false;
  682. }
  683. }