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.

actions.any.ts 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import _ from 'lodash';
  2. import { IReduxState, IStore } from '../../app/types';
  3. import { setPrejoinDisplayNameRequired } from '../../prejoin/actions.any';
  4. import { conferenceLeft, conferenceWillLeave } from '../conference/actions';
  5. import { getCurrentConference } from '../conference/functions';
  6. import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
  7. import {
  8. appendURLParam,
  9. getBackendSafeRoomName
  10. } from '../util/uri';
  11. import {
  12. CONNECTION_DISCONNECTED,
  13. CONNECTION_ESTABLISHED,
  14. CONNECTION_FAILED,
  15. CONNECTION_WILL_CONNECT,
  16. SET_LOCATION_URL
  17. } from './actionTypes';
  18. import { JITSI_CONNECTION_URL_KEY } from './constants';
  19. import logger from './logger';
  20. /**
  21. * The error structure passed to the {@link connectionFailed} action.
  22. *
  23. * Note there was an intention to make the error resemble an Error instance (to
  24. * the extent that jitsi-meet needs it).
  25. */
  26. export type ConnectionFailedError = {
  27. /**
  28. * The invalid credentials that were used to authenticate and the
  29. * authentication failed.
  30. */
  31. credentials?: {
  32. /**
  33. * The XMPP user's ID.
  34. */
  35. jid: string;
  36. /**
  37. * The XMPP user's password.
  38. */
  39. password: string;
  40. };
  41. /**
  42. * The details about the connection failed event.
  43. */
  44. details?: Object;
  45. /**
  46. * Error message.
  47. */
  48. message?: string;
  49. /**
  50. * One of {@link JitsiConnectionError} constants (defined in
  51. * lib-jitsi-meet).
  52. */
  53. name: string;
  54. /**
  55. * Indicates whether this event is recoverable or not.
  56. */
  57. recoverable?: boolean;
  58. };
  59. /**
  60. * Create an action for when the signaling connection has been lost.
  61. *
  62. * @param {JitsiConnection} connection - The {@code JitsiConnection} which
  63. * disconnected.
  64. * @private
  65. * @returns {{
  66. * type: CONNECTION_DISCONNECTED,
  67. * connection: JitsiConnection
  68. * }}
  69. */
  70. export function connectionDisconnected(connection?: Object) {
  71. return {
  72. type: CONNECTION_DISCONNECTED,
  73. connection
  74. };
  75. }
  76. /**
  77. * Create an action for when the signaling connection has been established.
  78. *
  79. * @param {JitsiConnection} connection - The {@code JitsiConnection} which was
  80. * established.
  81. * @param {number} timeEstablished - The time at which the
  82. * {@code JitsiConnection} which was established.
  83. * @public
  84. * @returns {{
  85. * type: CONNECTION_ESTABLISHED,
  86. * connection: JitsiConnection,
  87. * timeEstablished: number
  88. * }}
  89. */
  90. export function connectionEstablished(
  91. connection: Object, timeEstablished: number) {
  92. return {
  93. type: CONNECTION_ESTABLISHED,
  94. connection,
  95. timeEstablished
  96. };
  97. }
  98. /**
  99. * Create an action for when the signaling connection could not be created.
  100. *
  101. * @param {JitsiConnection} connection - The {@code JitsiConnection} which
  102. * failed.
  103. * @param {ConnectionFailedError} error - Error.
  104. * @public
  105. * @returns {{
  106. * type: CONNECTION_FAILED,
  107. * connection: JitsiConnection,
  108. * error: ConnectionFailedError
  109. * }}
  110. */
  111. export function connectionFailed(
  112. connection: Object,
  113. error: ConnectionFailedError) {
  114. const { credentials } = error;
  115. if (credentials && !Object.keys(credentials).length) {
  116. error.credentials = undefined;
  117. }
  118. return {
  119. type: CONNECTION_FAILED,
  120. connection,
  121. error
  122. };
  123. }
  124. /**
  125. * Constructs options to be passed to the constructor of {@code JitsiConnection}
  126. * based on the redux state.
  127. *
  128. * @param {Object} state - The redux state.
  129. * @returns {Object} The options to be passed to the constructor of
  130. * {@code JitsiConnection}.
  131. */
  132. export function constructOptions(state: IReduxState) {
  133. // Deep clone the options to make sure we don't modify the object in the
  134. // redux store.
  135. const options = _.cloneDeep(state['features/base/config']);
  136. const { bosh } = options;
  137. let { websocket } = options;
  138. // TESTING: Only enable WebSocket for some percentage of users.
  139. if (websocket && navigator.product === 'ReactNative') {
  140. if ((Math.random() * 100) >= (options?.testing?.mobileXmppWsThreshold ?? 0)) {
  141. websocket = undefined;
  142. }
  143. }
  144. // WebSocket is preferred over BOSH.
  145. const serviceUrl = websocket || bosh;
  146. logger.log(`Using service URL ${serviceUrl}`);
  147. // Append room to the URL's search.
  148. const { room } = state['features/base/conference'];
  149. if (serviceUrl && room) {
  150. const roomName = getBackendSafeRoomName(room);
  151. options.serviceUrl = appendURLParam(serviceUrl, 'room', roomName ?? '');
  152. if (options.websocketKeepAliveUrl) {
  153. options.websocketKeepAliveUrl = appendURLParam(options.websocketKeepAliveUrl, 'room', roomName ?? '');
  154. }
  155. if (options.conferenceRequestUrl) {
  156. options.conferenceRequestUrl = appendURLParam(options.conferenceRequestUrl, 'room', roomName ?? '');
  157. }
  158. }
  159. return options;
  160. }
  161. /**
  162. * Sets the location URL of the application, connection, conference, etc.
  163. *
  164. * @param {URL} [locationURL] - The location URL of the application,
  165. * connection, conference, etc.
  166. * @returns {{
  167. * type: SET_LOCATION_URL,
  168. * locationURL: URL
  169. * }}
  170. */
  171. export function setLocationURL(locationURL?: URL) {
  172. return {
  173. type: SET_LOCATION_URL,
  174. locationURL
  175. };
  176. }
  177. /**
  178. * Opens new connection.
  179. *
  180. * @param {string} [id] - The XMPP user's ID (e.g. {@code user@server.com}).
  181. * @param {string} [password] - The XMPP user's password.
  182. * @returns {Function}
  183. */
  184. export function _connectInternal(id?: string, password?: string) {
  185. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  186. const state = getState();
  187. const options = constructOptions(state);
  188. const { locationURL } = state['features/base/connection'];
  189. const { jwt } = state['features/base/jwt'];
  190. const connection = new JitsiMeetJS.JitsiConnection(options.appId, jwt, options);
  191. connection[JITSI_CONNECTION_URL_KEY] = locationURL;
  192. dispatch(_connectionWillConnect(connection));
  193. return new Promise((resolve, reject) => {
  194. connection.addEventListener(
  195. JitsiConnectionEvents.CONNECTION_DISCONNECTED,
  196. _onConnectionDisconnected);
  197. connection.addEventListener(
  198. JitsiConnectionEvents.CONNECTION_ESTABLISHED,
  199. _onConnectionEstablished);
  200. connection.addEventListener(
  201. JitsiConnectionEvents.CONNECTION_FAILED,
  202. _onConnectionFailed);
  203. /**
  204. * Marks the display name for the prejoin screen as required.
  205. * This can happen if a user tries to join a room with lobby enabled.
  206. */
  207. connection.addEventListener(
  208. JitsiConnectionEvents.DISPLAY_NAME_REQUIRED,
  209. () => dispatch(setPrejoinDisplayNameRequired())
  210. );
  211. /**
  212. * Unsubscribe the connection instance from
  213. * {@code CONNECTION_DISCONNECTED} and {@code CONNECTION_FAILED} events.
  214. *
  215. * @returns {void}
  216. */
  217. function unsubscribe() {
  218. connection.removeEventListener(
  219. JitsiConnectionEvents.CONNECTION_DISCONNECTED, _onConnectionDisconnected);
  220. connection.removeEventListener(JitsiConnectionEvents.CONNECTION_FAILED, _onConnectionFailed);
  221. }
  222. /**
  223. * Dispatches {@code CONNECTION_DISCONNECTED} action when connection is
  224. * disconnected.
  225. *
  226. * @private
  227. * @returns {void}
  228. */
  229. function _onConnectionDisconnected() {
  230. unsubscribe();
  231. dispatch(connectionDisconnected(connection));
  232. resolve(connection);
  233. }
  234. /**
  235. * Rejects external promise when connection fails.
  236. *
  237. * @param {JitsiConnectionErrors} err - Connection error.
  238. * @param {string} [message] - Error message supplied by lib-jitsi-meet.
  239. * @param {Object} [credentials] - The invalid credentials that were
  240. * used to authenticate and the authentication failed.
  241. * @param {string} [credentials.jid] - The XMPP user's ID.
  242. * @param {string} [credentials.password] - The XMPP user's password.
  243. * @param {Object} details - Additional information about the error.
  244. * @private
  245. * @returns {void}
  246. */
  247. function _onConnectionFailed( // eslint-disable-line max-params
  248. err: string,
  249. message: string,
  250. credentials: any,
  251. details: Object) {
  252. unsubscribe();
  253. dispatch(connectionFailed(connection, {
  254. credentials,
  255. details,
  256. name: err,
  257. message
  258. }));
  259. reject(err);
  260. }
  261. /**
  262. * Resolves external promise when connection is established.
  263. *
  264. * @private
  265. * @returns {void}
  266. */
  267. function _onConnectionEstablished() {
  268. connection.removeEventListener(JitsiConnectionEvents.CONNECTION_ESTABLISHED, _onConnectionEstablished);
  269. dispatch(connectionEstablished(connection, Date.now()));
  270. resolve(connection);
  271. }
  272. connection.connect({
  273. id,
  274. password
  275. });
  276. });
  277. };
  278. }
  279. /**
  280. * Create an action for when a connection will connect.
  281. *
  282. * @param {JitsiConnection} connection - The {@code JitsiConnection} which will
  283. * connect.
  284. * @private
  285. * @returns {{
  286. * type: CONNECTION_WILL_CONNECT,
  287. * connection: JitsiConnection
  288. * }}
  289. */
  290. function _connectionWillConnect(connection: Object) {
  291. return {
  292. type: CONNECTION_WILL_CONNECT,
  293. connection
  294. };
  295. }
  296. /**
  297. * Closes connection.
  298. *
  299. * @returns {Function}
  300. */
  301. export function disconnect() {
  302. return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<void> => {
  303. const state = getState();
  304. // The conference we have already joined or are joining.
  305. const conference_ = getCurrentConference(state);
  306. // Promise which completes when the conference has been left and the
  307. // connection has been disconnected.
  308. let promise;
  309. // Leave the conference.
  310. if (conference_) {
  311. // In a fashion similar to JitsiConference's CONFERENCE_LEFT event
  312. // (and the respective Redux action) which is fired after the
  313. // conference has been left, notify the application about the
  314. // intention to leave the conference.
  315. dispatch(conferenceWillLeave(conference_));
  316. promise
  317. = conference_.leave()
  318. .catch((error: Error) => {
  319. logger.warn(
  320. 'JitsiConference.leave() rejected with:',
  321. error);
  322. // The library lib-jitsi-meet failed to make the
  323. // JitsiConference leave. Which may be because
  324. // JitsiConference thinks it has already left.
  325. // Regardless of the failure reason, continue in
  326. // jitsi-meet as if the leave has succeeded.
  327. dispatch(conferenceLeft(conference_));
  328. });
  329. } else {
  330. promise = Promise.resolve();
  331. }
  332. // Disconnect the connection.
  333. const { connecting, connection } = state['features/base/connection'];
  334. // The connection we have already connected or are connecting.
  335. const connection_ = connection || connecting;
  336. if (connection_) {
  337. promise = promise.then(() => connection_.disconnect());
  338. } else {
  339. logger.info('No connection found while disconnecting.');
  340. }
  341. return promise;
  342. };
  343. }