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

middleware.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  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. setLobbyModeEnabled,
  35. startKnocking,
  36. setPasswordJoinFailed,
  37. setLobbyMessageListener
  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. batch(() => {
  88. dispatch(
  89. participantIsKnockingOrUpdated({
  90. id,
  91. name
  92. })
  93. );
  94. dispatch(playSound(KNOCKING_PARTICIPANT_SOUND_ID));
  95. const isParticipantsPaneVisible = getParticipantsPaneOpen(getState());
  96. if (navigator.product === 'ReactNative' || isParticipantsPaneVisible) {
  97. return;
  98. }
  99. _handleLobbyNotification({
  100. dispatch,
  101. getState
  102. });
  103. let notificationTitle;
  104. let customActionNameKey;
  105. let customActionHandler;
  106. let descriptionKey;
  107. let icon;
  108. const knockingParticipants = getKnockingParticipants(getState());
  109. const firstParticipant = knockingParticipants[0];
  110. const showChat = showLobbyChatButton(firstParticipant)(getState());
  111. if (knockingParticipants.length > 1) {
  112. descriptionKey = 'notify.participantsWantToJoin';
  113. notificationTitle = i18n.t('notify.waitingParticipants', {
  114. waitingParticipants: knockingParticipants.length
  115. });
  116. icon = NOTIFICATION_ICON.PARTICIPANTS;
  117. customActionNameKey = [ 'notify.viewLobby' ];
  118. customActionHandler = [ () => batch(() => {
  119. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  120. dispatch(openParticipantsPane());
  121. }) ];
  122. } else {
  123. descriptionKey = 'notify.participantWantsToJoin';
  124. notificationTitle = firstParticipant.name;
  125. icon = NOTIFICATION_ICON.PARTICIPANT;
  126. customActionNameKey = [ 'lobby.admit', 'lobby.reject' ];
  127. customActionHandler = [ () => batch(() => {
  128. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  129. dispatch(approveKnockingParticipant(firstParticipant.id));
  130. }),
  131. () => batch(() => {
  132. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  133. dispatch(rejectKnockingParticipant(firstParticipant.id));
  134. }) ];
  135. if (showChat) {
  136. customActionNameKey.splice(1, 0, 'lobby.chat');
  137. customActionHandler.splice(1, 0, () => batch(() => {
  138. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  139. dispatch(handleLobbyChatInitialized(firstParticipant.id));
  140. }));
  141. }
  142. }
  143. dispatch(showNotification({
  144. title: notificationTitle,
  145. descriptionKey,
  146. uid: LOBBY_NOTIFICATION_ID,
  147. customActionNameKey,
  148. customActionHandler,
  149. icon
  150. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  151. if (typeof APP !== 'undefined') {
  152. APP.API.notifyKnockingParticipant({
  153. id,
  154. name
  155. });
  156. }
  157. });
  158. });
  159. conference.on(JitsiConferenceEvents.LOBBY_USER_UPDATED, (id, participant) => {
  160. dispatch(
  161. participantIsKnockingOrUpdated({
  162. ...participant,
  163. id
  164. })
  165. );
  166. });
  167. conference.on(JitsiConferenceEvents.LOBBY_USER_LEFT, id => {
  168. batch(() => {
  169. dispatch(knockingParticipantLeft(id));
  170. dispatch(removeLobbyChatParticipant());
  171. dispatch(updateLobbyParticipantOnLeave(id));
  172. });
  173. });
  174. conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, (origin, sender) =>
  175. _maybeSendLobbyNotification(origin, sender, {
  176. dispatch,
  177. getState
  178. })
  179. );
  180. }
  181. }
  182. );
  183. /**
  184. * Function to handle the lobby notification.
  185. *
  186. * @param {Object} store - The Redux store.
  187. * @returns {void}
  188. */
  189. function _handleLobbyNotification(store) {
  190. const { dispatch, getState } = store;
  191. const knockingParticipants = getKnockingParticipants(getState());
  192. if (knockingParticipants.length === 0) {
  193. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  194. return;
  195. }
  196. let notificationTitle;
  197. let customActionNameKey;
  198. let customActionHandler;
  199. let descriptionKey;
  200. let icon;
  201. if (knockingParticipants.length === 1) {
  202. const firstParticipant = knockingParticipants[0];
  203. descriptionKey = 'notify.participantWantsToJoin';
  204. notificationTitle = firstParticipant.name;
  205. icon = NOTIFICATION_ICON.PARTICIPANT;
  206. customActionNameKey = [ 'lobby.admit', 'lobby.reject' ];
  207. customActionHandler = [ () => batch(() => {
  208. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  209. dispatch(approveKnockingParticipant(firstParticipant.id));
  210. }),
  211. () => batch(() => {
  212. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  213. dispatch(rejectKnockingParticipant(firstParticipant.id));
  214. }) ];
  215. } else {
  216. descriptionKey = 'notify.participantsWantToJoin';
  217. notificationTitle = i18n.t('notify.waitingParticipants', {
  218. waitingParticipants: knockingParticipants.length
  219. });
  220. icon = NOTIFICATION_ICON.PARTICIPANTS;
  221. customActionNameKey = [ 'notify.viewLobby' ];
  222. customActionHandler = [ () => batch(() => {
  223. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  224. dispatch(openParticipantsPane());
  225. }) ];
  226. }
  227. dispatch(showNotification({
  228. title: notificationTitle,
  229. descriptionKey,
  230. uid: LOBBY_NOTIFICATION_ID,
  231. customActionNameKey,
  232. customActionHandler,
  233. icon
  234. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  235. }
  236. /**
  237. * Function to handle the conference failed event and navigate the user to the lobby screen
  238. * based on the failure reason.
  239. *
  240. * @param {Object} store - The Redux store.
  241. * @param {Function} next - The Redux next function.
  242. * @param {Object} action - The Redux action.
  243. * @returns {Object}
  244. */
  245. function _conferenceFailed({ dispatch, getState }, next, action) {
  246. const { error } = action;
  247. const state = getState();
  248. const { membersOnly } = state['features/base/conference'];
  249. const nonFirstFailure = Boolean(membersOnly);
  250. if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) {
  251. if (typeof error.recoverable === 'undefined') {
  252. error.recoverable = true;
  253. }
  254. const result = next(action);
  255. dispatch(openLobbyScreen());
  256. if (shouldAutoKnock(state)) {
  257. dispatch(startKnocking());
  258. }
  259. // In case of wrong password we need to be in the right state if in the meantime someone allows us to join
  260. if (nonFirstFailure) {
  261. dispatch(conferenceWillJoin(membersOnly));
  262. }
  263. dispatch(setPasswordJoinFailed(nonFirstFailure));
  264. return result;
  265. }
  266. dispatch(hideLobbyScreen());
  267. if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
  268. dispatch(
  269. showNotification({
  270. appearance: NOTIFICATION_TYPE.ERROR,
  271. hideErrorSupportLink: true,
  272. titleKey: 'lobby.joinRejectedTitle',
  273. descriptionKey: 'lobby.joinRejectedMessage'
  274. }, NOTIFICATION_TIMEOUT_TYPE.LONG)
  275. );
  276. }
  277. return next(action);
  278. }
  279. /**
  280. * Handles cleanup of lobby state when a conference is joined.
  281. *
  282. * @param {Object} store - The Redux store.
  283. * @param {Function} next - The Redux next function.
  284. * @param {Object} action - The Redux action.
  285. * @returns {Object}
  286. */
  287. function _conferenceJoined({ dispatch }, next, action) {
  288. dispatch(hideLobbyScreen());
  289. return next(action);
  290. }
  291. /**
  292. * Finds the loadable avatar URL and updates the participant accordingly.
  293. *
  294. * @param {Object} store - The Redux store.
  295. * @param {Object} participant - The knocking participant.
  296. * @returns {void}
  297. */
  298. function _findLoadableAvatarForKnockingParticipant(store, { id }) {
  299. const { dispatch, getState } = store;
  300. const updatedParticipant = getState()['features/lobby'].knockingParticipants.find(p => p.id === id);
  301. const { disableThirdPartyRequests } = getState()['features/base/config'];
  302. if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
  303. getFirstLoadableAvatarUrl(updatedParticipant, store).then(result => {
  304. if (result) {
  305. const { isUsingCORS, src } = result;
  306. dispatch(
  307. participantIsKnockingOrUpdated({
  308. loadableAvatarUrl: src,
  309. id,
  310. isUsingCORS
  311. })
  312. );
  313. }
  314. });
  315. }
  316. }
  317. /**
  318. * Check the endpoint message that arrived through the conference and
  319. * sends a lobby notification, if the message belongs to the feature.
  320. *
  321. * @param {Object} origin - The origin (initiator) of the message.
  322. * @param {Object} message - The actual message.
  323. * @param {Object} store - The Redux store.
  324. * @returns {void}
  325. */
  326. function _maybeSendLobbyNotification(origin, message, { dispatch, getState }) {
  327. if (!origin?._id || message?.type !== 'lobby-notify') {
  328. return;
  329. }
  330. const notificationProps: any = {
  331. descriptionArguments: {
  332. originParticipantName: getParticipantDisplayName(getState, origin._id),
  333. targetParticipantName: message.name
  334. },
  335. titleKey: 'lobby.notificationTitle'
  336. };
  337. switch (message.event) {
  338. case 'LOBBY-ENABLED':
  339. notificationProps.descriptionKey = `lobby.notificationLobby${message.value ? 'En' : 'Dis'}abled`;
  340. break;
  341. case 'LOBBY-ACCESS-GRANTED':
  342. notificationProps.descriptionKey = 'lobby.notificationLobbyAccessGranted';
  343. break;
  344. case 'LOBBY-ACCESS-DENIED':
  345. notificationProps.descriptionKey = 'lobby.notificationLobbyAccessDenied';
  346. break;
  347. }
  348. dispatch(
  349. showNotification(
  350. notificationProps,
  351. isTestModeEnabled(getState()) ? NOTIFICATION_TIMEOUT_TYPE.STICKY : NOTIFICATION_TIMEOUT_TYPE.MEDIUM
  352. )
  353. );
  354. }