您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

middleware.ts 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  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 {
  7. CONFERENCE_FAILED,
  8. CONFERENCE_JOINED,
  9. ENDPOINT_MESSAGE_RECEIVED
  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. import { openChat } from '../chat/actions';
  30. import {
  31. handleLobbyChatInitialized,
  32. removeLobbyChatParticipant
  33. } from '../chat/actions.any';
  34. import { arePollsDisabled } from '../conference/functions.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 { PREJOIN_JOINING_IN_PROGRESS } from '../prejoin/actionTypes';
  46. import {
  47. isPrejoinEnabledInConfig,
  48. isPrejoinPageVisible,
  49. shouldAutoKnock
  50. } from '../prejoin/functions';
  51. import {
  52. KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
  53. KNOCKING_PARTICIPANT_LEFT
  54. } from './actionTypes';
  55. import {
  56. approveKnockingParticipant,
  57. hideLobbyScreen,
  58. knockingParticipantLeft,
  59. openLobbyScreen,
  60. participantIsKnockingOrUpdated,
  61. rejectKnockingParticipant,
  62. setLobbyMessageListener,
  63. setLobbyModeEnabled,
  64. setPasswordJoinFailed,
  65. startKnocking
  66. } from './actions';
  67. import { updateLobbyParticipantOnLeave } from './actions.any';
  68. import { KNOCKING_PARTICIPANT_SOUND_ID } from './constants';
  69. import { getKnockingParticipants, showLobbyChatButton } from './functions';
  70. import { KNOCKING_PARTICIPANT_FILE } from './sounds';
  71. import { IKnockingParticipant } from './types';
  72. MiddlewareRegistry.register(store => next => action => {
  73. switch (action.type) {
  74. case APP_WILL_MOUNT:
  75. store.dispatch(registerSound(KNOCKING_PARTICIPANT_SOUND_ID, KNOCKING_PARTICIPANT_FILE));
  76. break;
  77. case APP_WILL_UNMOUNT:
  78. store.dispatch(unregisterSound(KNOCKING_PARTICIPANT_SOUND_ID));
  79. break;
  80. case CONFERENCE_FAILED:
  81. return _conferenceFailed(store, next, action);
  82. case CONFERENCE_JOINED:
  83. return _conferenceJoined(store, next, action);
  84. case ENDPOINT_MESSAGE_RECEIVED: {
  85. const { participant, data } = action;
  86. _maybeSendLobbyNotification(participant, data, store);
  87. break;
  88. }
  89. case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED: {
  90. // We need the full update result to be in the store already
  91. const result = next(action);
  92. _findLoadableAvatarForKnockingParticipant(store, action.participant);
  93. _handleLobbyNotification(store);
  94. return result;
  95. }
  96. case KNOCKING_PARTICIPANT_LEFT: {
  97. // We need the full update result to be in the store already
  98. const result = next(action);
  99. _handleLobbyNotification(store);
  100. return result;
  101. }
  102. case PREJOIN_JOINING_IN_PROGRESS: {
  103. if (action.value) {
  104. // let's hide the notification (the case with denied access and retrying) when prejoin is enabled
  105. store.dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  106. }
  107. break;
  108. }
  109. }
  110. return next(action);
  111. });
  112. /**
  113. * Registers a change handler for state['features/base/conference'].conference to
  114. * set the event listeners needed for the lobby feature to operate.
  115. */
  116. StateListenerRegistry.register(
  117. state => state['features/base/conference'].conference,
  118. (conference, { dispatch, getState }, previousConference) => {
  119. if (conference && !previousConference) {
  120. conference.on(JitsiConferenceEvents.MEMBERS_ONLY_CHANGED, (enabled: boolean) => {
  121. dispatch(setLobbyModeEnabled(enabled));
  122. if (enabled) {
  123. dispatch(setLobbyMessageListener());
  124. }
  125. });
  126. conference.on(JitsiConferenceEvents.LOBBY_USER_JOINED, (id: string, name: string) => {
  127. const { soundsParticipantKnocking } = getState()['features/base/settings'];
  128. batch(() => {
  129. dispatch(
  130. participantIsKnockingOrUpdated({
  131. id,
  132. name
  133. })
  134. );
  135. if (soundsParticipantKnocking) {
  136. dispatch(playSound(KNOCKING_PARTICIPANT_SOUND_ID));
  137. }
  138. const isParticipantsPaneVisible = getParticipantsPaneOpen(getState());
  139. if (typeof APP !== 'undefined') {
  140. APP.API.notifyKnockingParticipant({
  141. id,
  142. name
  143. });
  144. }
  145. if (isParticipantsPaneVisible || navigator.product === 'ReactNative') {
  146. return;
  147. }
  148. _handleLobbyNotification({
  149. dispatch,
  150. getState
  151. });
  152. });
  153. });
  154. conference.on(JitsiConferenceEvents.LOBBY_USER_UPDATED, (id: string, participant: IKnockingParticipant) => {
  155. dispatch(
  156. participantIsKnockingOrUpdated({
  157. ...participant,
  158. id
  159. })
  160. );
  161. });
  162. conference.on(JitsiConferenceEvents.LOBBY_USER_LEFT, (id: string) => {
  163. batch(() => {
  164. dispatch(knockingParticipantLeft(id));
  165. dispatch(removeLobbyChatParticipant());
  166. dispatch(updateLobbyParticipantOnLeave(id));
  167. });
  168. });
  169. }
  170. }
  171. );
  172. /**
  173. * Function to handle the lobby notification.
  174. *
  175. * @param {Object} store - The Redux store.
  176. * @returns {void}
  177. */
  178. function _handleLobbyNotification(store: IStore) {
  179. const { dispatch, getState } = store;
  180. const knockingParticipants = getKnockingParticipants(getState());
  181. if (knockingParticipants.length === 0) {
  182. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  183. return;
  184. }
  185. let notificationTitle;
  186. let customActionNameKey;
  187. let customActionHandler;
  188. let customActionType;
  189. let descriptionKey;
  190. let icon;
  191. if (knockingParticipants.length === 1) {
  192. const firstParticipant = knockingParticipants[0];
  193. const showChat = showLobbyChatButton(firstParticipant)(getState());
  194. descriptionKey = 'notify.participantWantsToJoin';
  195. notificationTitle = firstParticipant.name;
  196. icon = NOTIFICATION_ICON.PARTICIPANT;
  197. customActionNameKey = [ 'participantsPane.actions.admit', 'participantsPane.actions.reject' ];
  198. customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
  199. customActionHandler = [ () => batch(() => {
  200. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  201. dispatch(approveKnockingParticipant(firstParticipant.id));
  202. }),
  203. () => batch(() => {
  204. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  205. dispatch(rejectKnockingParticipant(firstParticipant.id));
  206. }) ];
  207. // This checks if lobby chat button is available
  208. // and, if so, it adds it to the customActionNameKey array
  209. if (showChat) {
  210. customActionNameKey.splice(1, 0, 'lobby.chat');
  211. customActionType.splice(1, 0, BUTTON_TYPES.SECONDARY);
  212. customActionHandler.splice(1, 0, () => batch(() => {
  213. dispatch(handleLobbyChatInitialized(firstParticipant.id));
  214. dispatch(openChat({}, arePollsDisabled(getState())));
  215. }));
  216. }
  217. } else {
  218. descriptionKey = 'notify.participantsWantToJoin';
  219. notificationTitle = i18n.t('notify.waitingParticipants', {
  220. waitingParticipants: knockingParticipants.length
  221. });
  222. icon = NOTIFICATION_ICON.PARTICIPANTS;
  223. customActionNameKey = [ 'notify.viewLobby' ];
  224. customActionType = [ BUTTON_TYPES.PRIMARY ];
  225. customActionHandler = [ () => batch(() => {
  226. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  227. dispatch(openParticipantsPane());
  228. }) ];
  229. }
  230. dispatch(showNotification({
  231. title: notificationTitle,
  232. descriptionKey,
  233. uid: LOBBY_NOTIFICATION_ID,
  234. customActionNameKey,
  235. customActionType,
  236. customActionHandler,
  237. icon
  238. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  239. }
  240. /**
  241. * Function to handle the conference failed event and navigate the user to the lobby screen
  242. * based on the failure reason.
  243. *
  244. * @param {Object} store - The Redux store.
  245. * @param {Function} next - The Redux next function.
  246. * @param {Object} action - The Redux action.
  247. * @returns {Object}
  248. */
  249. function _conferenceFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
  250. const { error } = action;
  251. const state = getState();
  252. const { lobbyError, membersOnly } = state['features/base/conference'];
  253. const nonFirstFailure = Boolean(membersOnly);
  254. if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) {
  255. if (typeof error.recoverable === 'undefined') {
  256. error.recoverable = true;
  257. }
  258. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  259. const [ _lobbyJid, lobbyWaitingForHost ] = error.params;
  260. const result = next(action);
  261. dispatch(openLobbyScreen());
  262. // if there was an error about display name and pre-join is not enabled
  263. if (shouldAutoKnock(state)
  264. || (lobbyError && !isPrejoinEnabledInConfig(state))
  265. || lobbyWaitingForHost) {
  266. dispatch(startKnocking());
  267. }
  268. // In case of wrong password we need to be in the right state if in the meantime someone allows us to join
  269. if (nonFirstFailure) {
  270. dispatch(conferenceWillJoin(membersOnly));
  271. }
  272. dispatch(setPasswordJoinFailed(nonFirstFailure));
  273. return result;
  274. } else if (error.name === JitsiConferenceErrors.DISPLAY_NAME_REQUIRED) {
  275. const [ isLobbyEnabled ] = error.params;
  276. const result = next(action);
  277. // if the error is due to required display name because lobby is enabled for the room
  278. // if not showing the prejoin page then show lobby UI
  279. if (isLobbyEnabled && !isPrejoinPageVisible(state)) {
  280. dispatch(openLobbyScreen());
  281. }
  282. return result;
  283. }
  284. // if both are available pre-join is with priority (the case when pre-join is enabled)
  285. // when pre-join is disabled, and we are in lobby with error, we want to end up in lobby UI
  286. // instead of hiding it and showing conference UI. Still in lobby the user can retry
  287. // after we show the error notification
  288. if (isPrejoinPageVisible(state)) {
  289. dispatch(hideLobbyScreen());
  290. }
  291. // we want to finish this action before showing the notification
  292. // as the conference will be cleared which will clear all notifications, including this one
  293. const result = next(action);
  294. if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
  295. dispatch(
  296. showNotification({
  297. appearance: NOTIFICATION_TYPE.ERROR,
  298. hideErrorSupportLink: true,
  299. titleKey: 'lobby.joinRejectedTitle',
  300. uid: LOBBY_NOTIFICATION_ID,
  301. descriptionKey: 'lobby.joinRejectedMessage'
  302. }, NOTIFICATION_TIMEOUT_TYPE.STICKY)
  303. );
  304. }
  305. return result;
  306. }
  307. /**
  308. * Handles cleanup of lobby state when a conference is joined.
  309. *
  310. * @param {Object} store - The Redux store.
  311. * @param {Function} next - The Redux next function.
  312. * @param {Object} action - The Redux action.
  313. * @returns {Object}
  314. */
  315. function _conferenceJoined({ dispatch }: IStore, next: Function, action: AnyAction) {
  316. dispatch(hideLobbyScreen());
  317. return next(action);
  318. }
  319. /**
  320. * Finds the loadable avatar URL and updates the participant accordingly.
  321. *
  322. * @param {Object} store - The Redux store.
  323. * @param {Object} participant - The knocking participant.
  324. * @returns {void}
  325. */
  326. function _findLoadableAvatarForKnockingParticipant(store: IStore, { id }: { id: string; }) {
  327. const { dispatch, getState } = store;
  328. const updatedParticipant = getState()['features/lobby'].knockingParticipants.find(p => p.id === id);
  329. const { disableThirdPartyRequests } = getState()['features/base/config'];
  330. if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
  331. getFirstLoadableAvatarUrl(updatedParticipant, store).then((result: { isUsingCORS: boolean; src: string; }) => {
  332. if (result) {
  333. const { isUsingCORS, src } = result;
  334. dispatch(
  335. participantIsKnockingOrUpdated({
  336. loadableAvatarUrl: src,
  337. id,
  338. isUsingCORS
  339. })
  340. );
  341. }
  342. });
  343. }
  344. }
  345. /**
  346. * Check the endpoint message that arrived through the conference and
  347. * sends a lobby notification, if the message belongs to the feature.
  348. *
  349. * @param {Object} origin - The origin (initiator) of the message.
  350. * @param {Object} message - The actual message.
  351. * @param {Object} store - The Redux store.
  352. * @returns {void}
  353. */
  354. function _maybeSendLobbyNotification(origin: any, message: any, { dispatch, getState }: IStore) {
  355. if (!origin?._id || message?.type !== 'lobby-notify') {
  356. return;
  357. }
  358. const notificationProps: INotificationProps = {
  359. descriptionArguments: {
  360. originParticipantName: getParticipantDisplayName(getState, origin._id),
  361. targetParticipantName: message.name
  362. },
  363. titleKey: 'lobby.notificationTitle'
  364. };
  365. switch (message.event) {
  366. case 'LOBBY-ENABLED':
  367. notificationProps.descriptionKey = `lobby.notificationLobby${message.value ? 'En' : 'Dis'}abled`;
  368. break;
  369. case 'LOBBY-ACCESS-GRANTED':
  370. notificationProps.descriptionKey = 'lobby.notificationLobbyAccessGranted';
  371. break;
  372. case 'LOBBY-ACCESS-DENIED':
  373. notificationProps.descriptionKey = 'lobby.notificationLobbyAccessDenied';
  374. break;
  375. }
  376. dispatch(
  377. showNotification(
  378. notificationProps,
  379. isTestModeEnabled(getState()) ? NOTIFICATION_TIMEOUT_TYPE.STICKY : NOTIFICATION_TIMEOUT_TYPE.MEDIUM
  380. )
  381. );
  382. }