Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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