You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

middleware.ts 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. import i18n from 'i18next';
  2. import { batch } from 'react-redux';
  3. import { IStore } from '../app/types';
  4. import { IStateful } from '../base/app/types';
  5. import {
  6. CONFERENCE_JOINED,
  7. CONFERENCE_JOIN_IN_PROGRESS,
  8. ENDPOINT_MESSAGE_RECEIVED,
  9. UPDATE_CONFERENCE_METADATA
  10. } from '../base/conference/actionTypes';
  11. import { SET_CONFIG } from '../base/config/actionTypes';
  12. import { CONNECTION_FAILED } from '../base/connection/actionTypes';
  13. import { connect, setPreferVisitor } from '../base/connection/actions';
  14. import { disconnect } from '../base/connection/actions.any';
  15. import { openDialog } from '../base/dialog/actions';
  16. import { JitsiConferenceEvents, JitsiConnectionErrors } from '../base/lib-jitsi-meet';
  17. import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
  18. import { raiseHand } from '../base/participants/actions';
  19. import {
  20. getLocalParticipant,
  21. getParticipantById,
  22. isLocalParticipantModerator
  23. } from '../base/participants/functions';
  24. import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
  25. import { toState } from '../base/redux/functions';
  26. import { BUTTON_TYPES } from '../base/ui/constants.any';
  27. import { hideNotification, showNotification } from '../notifications/actions';
  28. import {
  29. NOTIFICATION_ICON,
  30. NOTIFICATION_TIMEOUT_TYPE,
  31. VISITORS_NOT_LIVE_NOTIFICATION_ID,
  32. VISITORS_PROMOTION_NOTIFICATION_ID
  33. } from '../notifications/constants';
  34. import { INotificationProps } from '../notifications/types';
  35. import { open as openParticipantsPane } from '../participants-pane/actions';
  36. import { joinConference } from '../prejoin/actions';
  37. import { UPDATE_VISITORS_IN_QUEUE_COUNT } from './actionTypes';
  38. import {
  39. approveRequest,
  40. clearPromotionRequest,
  41. denyRequest,
  42. goLive,
  43. promotionRequestReceived,
  44. setInVisitorsQueue,
  45. setVisitorDemoteActor,
  46. setVisitorsSupported,
  47. updateVisitorsCount,
  48. updateVisitorsInQueueCount
  49. } from './actions';
  50. import { JoinMeetingDialog } from './components';
  51. import { getPromotionRequests, getVisitorsCount, getVisitorsInQueueCount } from './functions';
  52. import logger from './logger';
  53. import { WebsocketClient } from './websocket-client';
  54. MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
  55. switch (action.type) {
  56. case CONFERENCE_JOIN_IN_PROGRESS: {
  57. const { conference } = action;
  58. conference.on(JitsiConferenceEvents.PROPERTIES_CHANGED, (properties: { 'visitor-count': number; }) => {
  59. const visitorCount = Number(properties?.['visitor-count']);
  60. if (!isNaN(visitorCount) && getVisitorsCount(getState) !== visitorCount) {
  61. dispatch(updateVisitorsCount(visitorCount));
  62. }
  63. });
  64. break;
  65. }
  66. case CONFERENCE_JOINED: {
  67. const { conference } = action;
  68. if (getState()['features/visitors'].iAmVisitor) {
  69. dispatch(openDialog(JoinMeetingDialog));
  70. const { demoteActorDisplayName } = getState()['features/visitors'];
  71. dispatch(setVisitorDemoteActor(undefined));
  72. const notificationParams: INotificationProps = {
  73. titleKey: 'visitors.notification.title',
  74. descriptionKey: 'visitors.notification.description'
  75. };
  76. if (demoteActorDisplayName) {
  77. notificationParams.descriptionKey = 'visitors.notification.demoteDescription';
  78. notificationParams.descriptionArguments = {
  79. actor: demoteActorDisplayName
  80. };
  81. }
  82. // check for demote actor and update notification
  83. dispatch(showNotification(notificationParams, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  84. } else {
  85. dispatch(setVisitorsSupported(conference.isVisitorsSupported()));
  86. conference.on(JitsiConferenceEvents.VISITORS_SUPPORTED_CHANGED, (value: boolean) => {
  87. dispatch(setVisitorsSupported(value));
  88. });
  89. }
  90. conference.on(JitsiConferenceEvents.VISITORS_MESSAGE, (
  91. msg: { action: string; actor: string; from: string; id: string; nick: string; on: boolean; }) => {
  92. if (msg.action === 'demote-request') {
  93. // we need it before the disconnect
  94. const participantById = getParticipantById(getState, msg.actor);
  95. const localParticipant = getLocalParticipant(getState);
  96. if (localParticipant && localParticipant.id === msg.id) {
  97. // handle demote
  98. dispatch(disconnect(true))
  99. .then(() => dispatch(setPreferVisitor(true)))
  100. .then(() => {
  101. // we need to set the name, so we can use it later in the notification
  102. if (participantById) {
  103. dispatch(setVisitorDemoteActor(participantById.name));
  104. }
  105. return dispatch(connect());
  106. });
  107. }
  108. } else if (msg.action === 'promotion-request') {
  109. const request = {
  110. from: msg.from,
  111. nick: msg.nick
  112. };
  113. if (msg.on) {
  114. dispatch(promotionRequestReceived(request));
  115. } else {
  116. dispatch(clearPromotionRequest(request));
  117. }
  118. _handlePromotionNotification({
  119. dispatch,
  120. getState
  121. });
  122. } else {
  123. logger.error('Unknown action:', msg.action);
  124. }
  125. });
  126. conference.on(JitsiConferenceEvents.VISITORS_REJECTION, () => {
  127. dispatch(raiseHand(false));
  128. });
  129. break;
  130. }
  131. case ENDPOINT_MESSAGE_RECEIVED: {
  132. const { data } = action;
  133. if (data?.action === 'promotion-response' && data.approved) {
  134. const request = getPromotionRequests(getState())
  135. .find((r: any) => r.from === data.id);
  136. request && dispatch(clearPromotionRequest(request));
  137. }
  138. break;
  139. }
  140. case CONNECTION_FAILED: {
  141. const { error } = action;
  142. if (error?.name !== JitsiConnectionErrors.NOT_LIVE_ERROR) {
  143. break;
  144. }
  145. const { hosts, visitors: visitorsConfig } = getState()['features/base/config'];
  146. const { locationURL, preferVisitor } = getState()['features/base/connection'];
  147. if (!visitorsConfig?.queueService || !locationURL || !preferVisitor) {
  148. break;
  149. }
  150. // let's subscribe for visitor waiting queue
  151. const { room } = getState()['features/base/conference'];
  152. const conferenceJid = `${room}@${hosts?.muc}`;
  153. WebsocketClient.getInstance()
  154. .connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
  155. `/secured/conference/visitor/topic.${conferenceJid}`,
  156. msg => {
  157. if ('status' in msg && msg.status === 'live') {
  158. logger.info('The conference is now live!');
  159. WebsocketClient.getInstance().disconnect()
  160. .then(() => {
  161. let delay = 0;
  162. // now let's connect to meeting
  163. if ('randomDelayMs' in msg) {
  164. delay = msg.randomDelayMs;
  165. }
  166. if (WebsocketClient.getInstance().connectCount > 1) {
  167. // if we keep connecting/disconnecting, let's slow it down
  168. delay = 30 * 1000;
  169. }
  170. setTimeout(() => {
  171. dispatch(joinConference());
  172. dispatch(setInVisitorsQueue(false));
  173. }, Math.random() * delay);
  174. });
  175. }
  176. },
  177. getState()['features/base/jwt'].jwt,
  178. () => {
  179. dispatch(setInVisitorsQueue(true));
  180. });
  181. break;
  182. }
  183. case PARTICIPANT_UPDATED: {
  184. const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
  185. if (visitorsConfig?.queueService && isLocalParticipantModerator(getState)) {
  186. const { metadata } = getState()['features/base/conference'];
  187. if (metadata?.visitors?.live === false && !WebsocketClient.getInstance().isActive()) {
  188. // when go live is available and false, we should subscribe
  189. // to the service if available to listen for waiting visitors
  190. _subscribeQueueStats(getState(), dispatch);
  191. }
  192. }
  193. break;
  194. }
  195. case SET_CONFIG: {
  196. const result = next(action);
  197. const { preferVisitor } = action.config;
  198. if (preferVisitor !== undefined) {
  199. setPreferVisitor(preferVisitor);
  200. }
  201. return result;
  202. }
  203. case UPDATE_CONFERENCE_METADATA: {
  204. const { metadata } = action;
  205. const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
  206. if (!visitorsConfig?.queueService) {
  207. break;
  208. }
  209. if (isLocalParticipantModerator(getState)) {
  210. if (metadata?.visitors?.live === false) {
  211. if (!WebsocketClient.getInstance().isActive()) {
  212. // if metadata go live changes to goLive false and local is moderator
  213. // we should subscribe to the service if available to listen for waiting visitors
  214. _subscribeQueueStats(getState(), dispatch);
  215. }
  216. _showNotLiveNotification(dispatch, getVisitorsInQueueCount(getState));
  217. } else if (metadata?.visitors?.live) {
  218. dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
  219. WebsocketClient.getInstance().disconnect();
  220. }
  221. }
  222. break;
  223. }
  224. case UPDATE_VISITORS_IN_QUEUE_COUNT: {
  225. _showNotLiveNotification(dispatch, action.count);
  226. break;
  227. }
  228. }
  229. return next(action);
  230. });
  231. /**
  232. * Shows a notification that the meeting is not live.
  233. *
  234. * @param {Dispatch} dispatch - The Redux dispatch function.
  235. * @param {number} count - The count of visitors waiting.
  236. * @returns {void}
  237. */
  238. function _showNotLiveNotification(dispatch: IStore['dispatch'], count: number): void {
  239. // let's show notification
  240. dispatch(showNotification({
  241. titleKey: 'notify.waitingVisitorsTitle',
  242. descriptionKey: 'notify.waitingVisitors',
  243. descriptionArguments: {
  244. waitingVisitors: count
  245. },
  246. disableClosing: true,
  247. uid: VISITORS_NOT_LIVE_NOTIFICATION_ID,
  248. customActionNameKey: [ 'participantsPane.actions.goLive' ],
  249. customActionType: [ BUTTON_TYPES.PRIMARY ],
  250. customActionHandler: [ () => batch(() => {
  251. dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
  252. dispatch(goLive());
  253. }) ],
  254. icon: NOTIFICATION_ICON.PARTICIPANTS
  255. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  256. }
  257. /**
  258. * Subscribe for moderator stats.
  259. *
  260. * @param {Function|Object} stateful - The redux store or {@code getState}
  261. * function.
  262. * @param {Dispatch} dispatch - The Redux dispatch function.
  263. * @returns {void}
  264. */
  265. function _subscribeQueueStats(stateful: IStateful, dispatch: IStore['dispatch']) {
  266. const { hosts } = toState(stateful)['features/base/config'];
  267. const { room } = toState(stateful)['features/base/conference'];
  268. const conferenceJid = `${room}@${hosts?.muc}`;
  269. const { visitors: visitorsConfig } = toState(stateful)['features/base/config'];
  270. WebsocketClient.getInstance()
  271. .connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
  272. `/secured/conference/state/topic.${conferenceJid}`,
  273. msg => {
  274. if ('visitorsWaiting' in msg) {
  275. dispatch(updateVisitorsInQueueCount(msg.visitorsWaiting));
  276. }
  277. },
  278. toState(stateful)['features/base/jwt'].jwt);
  279. }
  280. /**
  281. * Function to handle the promotion notification.
  282. *
  283. * @param {Object} store - The Redux store.
  284. * @returns {void}
  285. */
  286. function _handlePromotionNotification(
  287. { dispatch, getState }: { dispatch: IStore['dispatch']; getState: IStore['getState']; }) {
  288. const requests = getPromotionRequests(getState());
  289. if (requests.length === 0) {
  290. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  291. return;
  292. }
  293. let notificationTitle;
  294. let customActionNameKey;
  295. let customActionHandler;
  296. let customActionType;
  297. let descriptionKey;
  298. let icon;
  299. if (requests.length === 1) {
  300. const firstRequest = requests[0];
  301. descriptionKey = 'notify.participantWantsToJoin';
  302. notificationTitle = firstRequest.nick;
  303. icon = NOTIFICATION_ICON.PARTICIPANT;
  304. customActionNameKey = [ 'participantsPane.actions.admit', 'participantsPane.actions.reject' ];
  305. customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
  306. customActionHandler = [ () => batch(() => {
  307. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  308. dispatch(approveRequest(firstRequest));
  309. }),
  310. () => batch(() => {
  311. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  312. dispatch(denyRequest(firstRequest));
  313. }) ];
  314. } else {
  315. descriptionKey = 'notify.participantsWantToJoin';
  316. notificationTitle = i18n.t('notify.waitingParticipants', {
  317. waitingParticipants: requests.length
  318. });
  319. icon = NOTIFICATION_ICON.PARTICIPANTS;
  320. customActionNameKey = [ 'notify.viewVisitors' ];
  321. customActionType = [ BUTTON_TYPES.PRIMARY ];
  322. customActionHandler = [ () => batch(() => {
  323. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  324. dispatch(openParticipantsPane());
  325. }) ];
  326. }
  327. dispatch(showNotification({
  328. title: notificationTitle,
  329. descriptionKey,
  330. uid: VISITORS_PROMOTION_NOTIFICATION_ID,
  331. customActionNameKey,
  332. customActionType,
  333. customActionHandler,
  334. icon
  335. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  336. }