Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

middleware.ts 14KB

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