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

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