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

middleware.js 14KB

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