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

middleware.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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 { approveKnockingParticipant, rejectKnockingParticipant } from '../lobby/actions';
  16. import {
  17. LOBBY_NOTIFICATION_ID,
  18. NOTIFICATION_ICON,
  19. NOTIFICATION_TIMEOUT_TYPE,
  20. NOTIFICATION_TYPE,
  21. hideNotification,
  22. showNotification
  23. } from '../notifications';
  24. import { open as openParticipantsPane } from '../participants-pane/actions';
  25. import { getParticipantsPaneOpen } from '../participants-pane/functions';
  26. import { shouldAutoKnock } from '../prejoin/functions';
  27. import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED } from './actionTypes';
  28. import {
  29. hideLobbyScreen,
  30. knockingParticipantLeft,
  31. openLobbyScreen,
  32. participantIsKnockingOrUpdated,
  33. setLobbyModeEnabled,
  34. startKnocking,
  35. setPasswordJoinFailed
  36. } from './actions';
  37. import { KNOCKING_PARTICIPANT_SOUND_ID } from './constants';
  38. import { getKnockingParticipants } from './functions';
  39. import { KNOCKING_PARTICIPANT_FILE } from './sounds';
  40. declare var APP: Object;
  41. MiddlewareRegistry.register(store => next => action => {
  42. switch (action.type) {
  43. case APP_WILL_MOUNT:
  44. store.dispatch(registerSound(KNOCKING_PARTICIPANT_SOUND_ID, KNOCKING_PARTICIPANT_FILE));
  45. break;
  46. case APP_WILL_UNMOUNT:
  47. store.dispatch(unregisterSound(KNOCKING_PARTICIPANT_SOUND_ID));
  48. break;
  49. case CONFERENCE_FAILED:
  50. return _conferenceFailed(store, next, action);
  51. case CONFERENCE_JOINED:
  52. return _conferenceJoined(store, next, action);
  53. case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED: {
  54. // We need the full update result to be in the store already
  55. const result = next(action);
  56. _findLoadableAvatarForKnockingParticipant(store, action.participant);
  57. return result;
  58. }
  59. }
  60. return next(action);
  61. });
  62. /**
  63. * Registers a change handler for state['features/base/conference'].conference to
  64. * set the event listeners needed for the lobby feature to operate.
  65. */
  66. StateListenerRegistry.register(
  67. state => state['features/base/conference'].conference,
  68. (conference, { dispatch, getState }, previousConference) => {
  69. if (conference && !previousConference) {
  70. conference.on(JitsiConferenceEvents.MEMBERS_ONLY_CHANGED, enabled => {
  71. dispatch(setLobbyModeEnabled(enabled));
  72. });
  73. conference.on(JitsiConferenceEvents.LOBBY_USER_JOINED, (id, name) => {
  74. batch(() => {
  75. dispatch(
  76. participantIsKnockingOrUpdated({
  77. id,
  78. name
  79. })
  80. );
  81. dispatch(playSound(KNOCKING_PARTICIPANT_SOUND_ID));
  82. const isParticipantsPaneVisible = getParticipantsPaneOpen(getState());
  83. if (navigator.product === 'ReactNative' || isParticipantsPaneVisible) {
  84. return;
  85. }
  86. let notificationTitle;
  87. let customActionNameKey;
  88. let customActionHandler;
  89. let descriptionKey;
  90. let icon;
  91. const knockingParticipants = getKnockingParticipants(getState());
  92. const firstParticipant = knockingParticipants[0];
  93. if (knockingParticipants.length > 1) {
  94. descriptionKey = 'notify.participantsWantToJoin';
  95. notificationTitle = i18n.t('notify.waitingParticipants', {
  96. waitingParticipants: knockingParticipants.length
  97. });
  98. icon = NOTIFICATION_ICON.PARTICIPANTS;
  99. customActionNameKey = [ 'notify.viewLobby' ];
  100. customActionHandler = [ () => batch(() => {
  101. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  102. dispatch(openParticipantsPane());
  103. }) ];
  104. } else {
  105. descriptionKey = 'notify.participantWantsToJoin';
  106. notificationTitle = firstParticipant.name;
  107. icon = NOTIFICATION_ICON.PARTICIPANT;
  108. customActionNameKey = [ 'lobby.admit', 'lobby.reject' ];
  109. customActionHandler = [ () => batch(() => {
  110. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  111. dispatch(approveKnockingParticipant(firstParticipant.id));
  112. }),
  113. () => batch(() => {
  114. dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
  115. dispatch(rejectKnockingParticipant(firstParticipant.id));
  116. }) ];
  117. }
  118. dispatch(showNotification({
  119. title: notificationTitle,
  120. descriptionKey,
  121. uid: LOBBY_NOTIFICATION_ID,
  122. customActionNameKey,
  123. customActionHandler,
  124. icon
  125. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  126. if (typeof APP !== 'undefined') {
  127. APP.API.notifyKnockingParticipant({
  128. id,
  129. name
  130. });
  131. }
  132. });
  133. });
  134. conference.on(JitsiConferenceEvents.LOBBY_USER_UPDATED, (id, participant) => {
  135. dispatch(
  136. participantIsKnockingOrUpdated({
  137. ...participant,
  138. id
  139. })
  140. );
  141. });
  142. conference.on(JitsiConferenceEvents.LOBBY_USER_LEFT, id => {
  143. dispatch(knockingParticipantLeft(id));
  144. });
  145. conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, (origin, sender) =>
  146. _maybeSendLobbyNotification(origin, sender, {
  147. dispatch,
  148. getState
  149. })
  150. );
  151. }
  152. }
  153. );
  154. /**
  155. * Function to handle the conference failed event and navigate the user to the lobby screen
  156. * based on the failure reason.
  157. *
  158. * @param {Object} store - The Redux store.
  159. * @param {Function} next - The Redux next function.
  160. * @param {Object} action - The Redux action.
  161. * @returns {Object}
  162. */
  163. function _conferenceFailed({ dispatch, getState }, next, action) {
  164. const { error } = action;
  165. const state = getState();
  166. const { membersOnly } = state['features/base/conference'];
  167. const nonFirstFailure = Boolean(membersOnly);
  168. if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) {
  169. if (typeof error.recoverable === 'undefined') {
  170. error.recoverable = true;
  171. }
  172. const result = next(action);
  173. dispatch(openLobbyScreen());
  174. if (shouldAutoKnock(state)) {
  175. dispatch(startKnocking());
  176. }
  177. // In case of wrong password we need to be in the right state if in the meantime someone allows us to join
  178. if (nonFirstFailure) {
  179. dispatch(conferenceWillJoin(membersOnly));
  180. }
  181. dispatch(setPasswordJoinFailed(nonFirstFailure));
  182. return result;
  183. }
  184. dispatch(hideLobbyScreen());
  185. if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
  186. dispatch(
  187. showNotification({
  188. appearance: NOTIFICATION_TYPE.ERROR,
  189. hideErrorSupportLink: true,
  190. titleKey: 'lobby.joinRejectedMessage'
  191. }, NOTIFICATION_TIMEOUT_TYPE.LONG)
  192. );
  193. }
  194. return next(action);
  195. }
  196. /**
  197. * Handles cleanup of lobby state when a conference is joined.
  198. *
  199. * @param {Object} store - The Redux store.
  200. * @param {Function} next - The Redux next function.
  201. * @param {Object} action - The Redux action.
  202. * @returns {Object}
  203. */
  204. function _conferenceJoined({ dispatch }, next, action) {
  205. dispatch(hideLobbyScreen());
  206. return next(action);
  207. }
  208. /**
  209. * Finds the loadable avatar URL and updates the participant accordingly.
  210. *
  211. * @param {Object} store - The Redux store.
  212. * @param {Object} participant - The knocking participant.
  213. * @returns {void}
  214. */
  215. function _findLoadableAvatarForKnockingParticipant(store, { id }) {
  216. const { dispatch, getState } = store;
  217. const updatedParticipant = getState()['features/lobby'].knockingParticipants.find(p => p.id === id);
  218. const { disableThirdPartyRequests } = getState()['features/base/config'];
  219. if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
  220. getFirstLoadableAvatarUrl(updatedParticipant, store).then(result => {
  221. if (result) {
  222. const { isUsingCORS, src } = result;
  223. dispatch(
  224. participantIsKnockingOrUpdated({
  225. loadableAvatarUrl: src,
  226. id,
  227. isUsingCORS
  228. })
  229. );
  230. }
  231. });
  232. }
  233. }
  234. /**
  235. * Check the endpoint message that arrived through the conference and
  236. * sends a lobby notification, if the message belongs to the feature.
  237. *
  238. * @param {Object} origin - The origin (initiator) of the message.
  239. * @param {Object} message - The actual message.
  240. * @param {Object} store - The Redux store.
  241. * @returns {void}
  242. */
  243. function _maybeSendLobbyNotification(origin, message, { dispatch, getState }) {
  244. if (!origin?._id || message?.type !== 'lobby-notify') {
  245. return;
  246. }
  247. const notificationProps: any = {
  248. descriptionArguments: {
  249. originParticipantName: getParticipantDisplayName(getState, origin._id),
  250. targetParticipantName: message.name
  251. },
  252. titleKey: 'lobby.notificationTitle'
  253. };
  254. switch (message.event) {
  255. case 'LOBBY-ENABLED':
  256. notificationProps.descriptionKey = `lobby.notificationLobby${message.value ? 'En' : 'Dis'}abled`;
  257. break;
  258. case 'LOBBY-ACCESS-GRANTED':
  259. notificationProps.descriptionKey = 'lobby.notificationLobbyAccessGranted';
  260. break;
  261. case 'LOBBY-ACCESS-DENIED':
  262. notificationProps.descriptionKey = 'lobby.notificationLobbyAccessDenied';
  263. break;
  264. }
  265. dispatch(
  266. showNotification(
  267. notificationProps,
  268. isTestModeEnabled(getState()) ? NOTIFICATION_TIMEOUT_TYPE.STICKY : NOTIFICATION_TIMEOUT_TYPE.MEDIUM
  269. )
  270. );
  271. }