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 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. import i18n from 'i18next';
  2. import { batch } from 'react-redux';
  3. import { AnyAction } from 'redux';
  4. import { IStore } from '../app/types';
  5. import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
  6. import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference/actionTypes';
  7. import { conferenceWillJoin } from '../base/conference/actions';
  8. import { JitsiConferenceErrors, JitsiConferenceEvents } from '../base/lib-jitsi-meet';
  9. import { getFirstLoadableAvatarUrl, getParticipantDisplayName } from '../base/participants/functions';
  10. import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
  11. import StateListenerRegistry from '../base/redux/StateListenerRegistry';
  12. import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
  13. import { isTestModeEnabled } from '../base/testing/functions';
  14. import { handleLobbyChatInitialized, removeLobbyChatParticipant } from '../chat/actions.any';
  15. import {
  16. hideNotification,
  17. showNotification
  18. } from '../notifications/actions';
  19. import {
  20. LOBBY_NOTIFICATION_ID,
  21. NOTIFICATION_ICON,
  22. NOTIFICATION_TIMEOUT_TYPE,
  23. NOTIFICATION_TYPE
  24. } from '../notifications/constants';
  25. import { INotificationProps } from '../notifications/types';
  26. import { open as openParticipantsPane } from '../participants-pane/actions';
  27. import { getParticipantsPaneOpen } from '../participants-pane/functions';
  28. import { shouldAutoKnock } from '../prejoin/functions';
  29. import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED, KNOCKING_PARTICIPANT_LEFT } from './actionTypes';
  30. import {
  31. approveKnockingParticipant,
  32. hideLobbyScreen,
  33. knockingParticipantLeft,
  34. openLobbyScreen,
  35. participantIsKnockingOrUpdated,
  36. rejectKnockingParticipant,
  37. setLobbyMessageListener,
  38. setLobbyModeEnabled,
  39. setPasswordJoinFailed,
  40. startKnocking
  41. } from './actions';
  42. import { updateLobbyParticipantOnLeave } from './actions.any';
  43. import { KNOCKING_PARTICIPANT_SOUND_ID } from './constants';
  44. import { getKnockingParticipants, showLobbyChatButton } from './functions';
  45. import { KNOCKING_PARTICIPANT_FILE } from './sounds';
  46. import { IKnockingParticipant } from './types';
  47. MiddlewareRegistry.register(store => next => action => {
  48. switch (action.type) {
  49. case APP_WILL_MOUNT:
  50. store.dispatch(registerSound(KNOCKING_PARTICIPANT_SOUND_ID, KNOCKING_PARTICIPANT_FILE));
  51. break;
  52. case APP_WILL_UNMOUNT:
  53. store.dispatch(unregisterSound(KNOCKING_PARTICIPANT_SOUND_ID));
  54. break;
  55. case CONFERENCE_FAILED:
  56. return _conferenceFailed(store, next, action);
  57. case CONFERENCE_JOINED:
  58. return _conferenceJoined(store, next, action);
  59. case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED: {
  60. // We need the full update result to be in the store already
  61. const result = next(action);
  62. _findLoadableAvatarForKnockingParticipant(store, action.participant);
  63. _handleLobbyNotification(store);
  64. return result;
  65. }
  66. case KNOCKING_PARTICIPANT_LEFT: {
  67. // We need the full update result to be in the store already
  68. const result = next(action);
  69. _handleLobbyNotification(store);
  70. return result;
  71. }
  72. }
  73. return next(action);
  74. });
  75. /**
  76. * Registers a change handler for state['features/base/conference'].conference to
  77. * set the event listeners needed for the lobby feature to operate.
  78. */
  79. StateListenerRegistry.register(
  80. state => state['features/base/conference'].conference,
  81. (conference, { dispatch, getState }, previousConference) => {
  82. if (conference && !previousConference) {
  83. conference.on(JitsiConferenceEvents.MEMBERS_ONLY_CHANGED, (enabled: boolean) => {
  84. dispatch(setLobbyModeEnabled(enabled));
  85. if (enabled) {
  86. dispatch(setLobbyMessageListener());
  87. }
  88. });
  89. conference.on(JitsiConferenceEvents.LOBBY_USER_JOINED, (id: string, name: string) => {
  90. const { soundsParticipantKnocking } = getState()['features/base/settings'];
  91. batch(() => {
  92. dispatch(
  93. participantIsKnockingOrUpdated({
  94. id,
  95. name
  96. })
  97. );
  98. if (soundsParticipantKnocking) {
  99. dispatch(playSound(KNOCKING_PARTICIPANT_SOUND_ID));
  100. }
  101. const isParticipantsPaneVisible = getParticipantsPaneOpen(getState());
  102. if (navigator.product === 'ReactNative' || isParticipantsPaneVisible) {
  103. return;
  104. }
  105. _handleLobbyNotification({
  106. dispatch,
  107. getState
  108. });
  109. let notificationTitle;
  110. let customActionNameKey;
  111. let customActionHandler;
  112. let descriptionKey;
  113. let icon;
  114. const knockingParticipants = getKnockingParticipants(getState());
  115. const firstParticipant = knockingParticipants[0];
  116. const showChat = showLobbyChatButton(firstParticipant)(getState());
  117. if (knockingParticipants.length > 1) {
  118. descriptionKey = 'notify.participantsWantToJoin';
  119. notificationTitle = i18n.t('notify.waitingParticipants', {
  120. waitingParticipants: knockingParticipants.length
  121. });
  122. icon = NOTIFICATION_ICON.PARTICIPANTS;
  123. customActionNameKey = [ 'notify.viewLobby' ];
  124. customActionHandler = [ () => batch(() => {
  125. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  126. dispatch(openParticipantsPane());
  127. }) ];
  128. } else {
  129. descriptionKey = 'notify.participantWantsToJoin';
  130. notificationTitle = firstParticipant.name;
  131. icon = NOTIFICATION_ICON.PARTICIPANT;
  132. customActionNameKey = [ 'lobby.admit', 'lobby.reject' ];
  133. customActionHandler = [ () => batch(() => {
  134. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  135. dispatch(approveKnockingParticipant(firstParticipant.id));
  136. }),
  137. () => batch(() => {
  138. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  139. dispatch(rejectKnockingParticipant(firstParticipant.id));
  140. }) ];
  141. if (showChat) {
  142. customActionNameKey.splice(1, 0, 'lobby.chat');
  143. customActionHandler.splice(1, 0, () => batch(() => {
  144. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  145. dispatch(handleLobbyChatInitialized(firstParticipant.id));
  146. }));
  147. }
  148. }
  149. dispatch(showNotification({
  150. title: notificationTitle,
  151. descriptionKey,
  152. uid: LOBBY_NOTIFICATION_ID,
  153. customActionNameKey,
  154. customActionHandler,
  155. icon
  156. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  157. if (typeof APP !== 'undefined') {
  158. APP.API.notifyKnockingParticipant({
  159. id,
  160. name
  161. });
  162. }
  163. });
  164. });
  165. conference.on(JitsiConferenceEvents.LOBBY_USER_UPDATED, (id: string, participant: IKnockingParticipant) => {
  166. dispatch(
  167. participantIsKnockingOrUpdated({
  168. ...participant,
  169. id
  170. })
  171. );
  172. });
  173. conference.on(JitsiConferenceEvents.LOBBY_USER_LEFT, (id: string) => {
  174. batch(() => {
  175. dispatch(knockingParticipantLeft(id));
  176. dispatch(removeLobbyChatParticipant());
  177. dispatch(updateLobbyParticipantOnLeave(id));
  178. });
  179. });
  180. conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, (origin: any, sender: any) =>
  181. _maybeSendLobbyNotification(origin, sender, {
  182. dispatch,
  183. getState
  184. })
  185. );
  186. }
  187. }
  188. );
  189. /**
  190. * Function to handle the lobby notification.
  191. *
  192. * @param {Object} store - The Redux store.
  193. * @returns {void}
  194. */
  195. function _handleLobbyNotification(store: IStore) {
  196. const { dispatch, getState } = store;
  197. const knockingParticipants = getKnockingParticipants(getState());
  198. if (knockingParticipants.length === 0) {
  199. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  200. return;
  201. }
  202. let notificationTitle;
  203. let customActionNameKey;
  204. let customActionHandler;
  205. let descriptionKey;
  206. let icon;
  207. if (knockingParticipants.length === 1) {
  208. const firstParticipant = knockingParticipants[0];
  209. descriptionKey = 'notify.participantWantsToJoin';
  210. notificationTitle = firstParticipant.name;
  211. icon = NOTIFICATION_ICON.PARTICIPANT;
  212. customActionNameKey = [ 'lobby.admit', 'lobby.reject' ];
  213. customActionHandler = [ () => batch(() => {
  214. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  215. dispatch(approveKnockingParticipant(firstParticipant.id));
  216. }),
  217. () => batch(() => {
  218. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  219. dispatch(rejectKnockingParticipant(firstParticipant.id));
  220. }) ];
  221. } else {
  222. descriptionKey = 'notify.participantsWantToJoin';
  223. notificationTitle = i18n.t('notify.waitingParticipants', {
  224. waitingParticipants: knockingParticipants.length
  225. });
  226. icon = NOTIFICATION_ICON.PARTICIPANTS;
  227. customActionNameKey = [ 'notify.viewLobby' ];
  228. customActionHandler = [ () => batch(() => {
  229. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  230. dispatch(openParticipantsPane());
  231. }) ];
  232. }
  233. dispatch(showNotification({
  234. title: notificationTitle,
  235. descriptionKey,
  236. uid: LOBBY_NOTIFICATION_ID,
  237. customActionNameKey,
  238. customActionHandler,
  239. icon
  240. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  241. }
  242. /**
  243. * Function to handle the conference failed event and navigate the user to the lobby screen
  244. * based on the failure reason.
  245. *
  246. * @param {Object} store - The Redux store.
  247. * @param {Function} next - The Redux next function.
  248. * @param {Object} action - The Redux action.
  249. * @returns {Object}
  250. */
  251. function _conferenceFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  252. const { error } = action;
  253. const state = getState();
  254. const { membersOnly } = state['features/base/conference'];
  255. const nonFirstFailure = Boolean(membersOnly);
  256. if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) {
  257. if (typeof error.recoverable === 'undefined') {
  258. error.recoverable = true;
  259. }
  260. const result = next(action);
  261. dispatch(openLobbyScreen());
  262. if (shouldAutoKnock(state)) {
  263. dispatch(startKnocking());
  264. }
  265. // In case of wrong password we need to be in the right state if in the meantime someone allows us to join
  266. if (nonFirstFailure) {
  267. // @ts-ignore
  268. dispatch(conferenceWillJoin(membersOnly));
  269. }
  270. dispatch(setPasswordJoinFailed(nonFirstFailure));
  271. return result;
  272. }
  273. dispatch(hideLobbyScreen());
  274. if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
  275. dispatch(
  276. showNotification({
  277. appearance: NOTIFICATION_TYPE.ERROR,
  278. hideErrorSupportLink: true,
  279. titleKey: 'lobby.joinRejectedTitle',
  280. descriptionKey: 'lobby.joinRejectedMessage'
  281. }, NOTIFICATION_TIMEOUT_TYPE.LONG)
  282. );
  283. }
  284. return next(action);
  285. }
  286. /**
  287. * Handles cleanup of lobby state when a conference is joined.
  288. *
  289. * @param {Object} store - The Redux store.
  290. * @param {Function} next - The Redux next function.
  291. * @param {Object} action - The Redux action.
  292. * @returns {Object}
  293. */
  294. function _conferenceJoined({ dispatch }: IStore, next: Function, action: AnyAction) {
  295. dispatch(hideLobbyScreen());
  296. return next(action);
  297. }
  298. /**
  299. * Finds the loadable avatar URL and updates the participant accordingly.
  300. *
  301. * @param {Object} store - The Redux store.
  302. * @param {Object} participant - The knocking participant.
  303. * @returns {void}
  304. */
  305. function _findLoadableAvatarForKnockingParticipant(store: IStore, { id }: { id: string; }) {
  306. const { dispatch, getState } = store;
  307. const updatedParticipant = getState()['features/lobby'].knockingParticipants.find(p => p.id === id);
  308. const { disableThirdPartyRequests } = getState()['features/base/config'];
  309. if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
  310. getFirstLoadableAvatarUrl(updatedParticipant, store).then((result: { isUsingCORS: boolean; src: string; }) => {
  311. if (result) {
  312. const { isUsingCORS, src } = result;
  313. dispatch(
  314. participantIsKnockingOrUpdated({
  315. loadableAvatarUrl: src,
  316. id,
  317. isUsingCORS
  318. })
  319. );
  320. }
  321. });
  322. }
  323. }
  324. /**
  325. * Check the endpoint message that arrived through the conference and
  326. * sends a lobby notification, if the message belongs to the feature.
  327. *
  328. * @param {Object} origin - The origin (initiator) of the message.
  329. * @param {Object} message - The actual message.
  330. * @param {Object} store - The Redux store.
  331. * @returns {void}
  332. */
  333. function _maybeSendLobbyNotification(origin: any, message: any, { dispatch, getState }: IStore) {
  334. if (!origin?._id || message?.type !== 'lobby-notify') {
  335. return;
  336. }
  337. const notificationProps: INotificationProps = {
  338. descriptionArguments: {
  339. originParticipantName: getParticipantDisplayName(getState, origin._id),
  340. targetParticipantName: message.name
  341. },
  342. titleKey: 'lobby.notificationTitle'
  343. };
  344. switch (message.event) {
  345. case 'LOBBY-ENABLED':
  346. notificationProps.descriptionKey = `lobby.notificationLobby${message.value ? 'En' : 'Dis'}abled`;
  347. break;
  348. case 'LOBBY-ACCESS-GRANTED':
  349. notificationProps.descriptionKey = 'lobby.notificationLobbyAccessGranted';
  350. break;
  351. case 'LOBBY-ACCESS-DENIED':
  352. notificationProps.descriptionKey = 'lobby.notificationLobbyAccessDenied';
  353. break;
  354. }
  355. dispatch(
  356. showNotification(
  357. notificationProps,
  358. isTestModeEnabled(getState()) ? NOTIFICATION_TIMEOUT_TYPE.STICKY : NOTIFICATION_TIMEOUT_TYPE.MEDIUM
  359. )
  360. );
  361. }