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.js 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. // @flow
  2. import { CONFERENCE_WILL_JOIN } from '../base/conference/actionTypes';
  3. import {
  4. getParticipantById,
  5. getPinnedParticipant,
  6. PARTICIPANT_LEFT,
  7. pinParticipant
  8. } from '../base/participants';
  9. import { MiddlewareRegistry } from '../base/redux';
  10. import { setFilmstripVisible } from '../filmstrip';
  11. import { setTileView } from '../video-layout';
  12. import {
  13. setFollowMeModerator,
  14. setFollowMeState
  15. } from './actions';
  16. import { FOLLOW_ME_COMMAND } from './constants';
  17. import { isFollowMeActive } from './functions';
  18. import logger from './logger';
  19. import './subscriber';
  20. declare var APP: Object;
  21. /**
  22. * The timeout after which a follow-me command that has been received will be
  23. * ignored if not consumed.
  24. *
  25. * @type {number} in seconds
  26. * @private
  27. */
  28. const _FOLLOW_ME_RECEIVED_TIMEOUT = 30;
  29. /**
  30. * An instance of a timeout used as a workaround when attempting to pin a
  31. * non-existent particapant, which may be caused by participant join information
  32. * not being received yet.
  33. *
  34. * @type {TimeoutID}
  35. */
  36. let nextOnStageTimeout;
  37. /**
  38. * A count of how many seconds the nextOnStageTimeout has ticked while waiting
  39. * for a participant to be discovered that should be pinned. This variable
  40. * works in conjunction with {@code _FOLLOW_ME_RECEIVED_TIMEOUT} and
  41. * {@code nextOnStageTimeout}.
  42. *
  43. * @type {number}
  44. */
  45. let nextOnStageTimer = 0;
  46. /**
  47. * Represents "Follow Me" feature which enables a moderator to (partially)
  48. * control the user experience/interface (e.g. filmstrip visibility) of (other)
  49. * non-moderator participant.
  50. */
  51. MiddlewareRegistry.register(store => next => action => {
  52. switch (action.type) {
  53. case CONFERENCE_WILL_JOIN: {
  54. const { conference } = action;
  55. conference.addCommandListener(
  56. FOLLOW_ME_COMMAND, ({ attributes }, id) => {
  57. _onFollowMeCommand(attributes, id, store);
  58. });
  59. break;
  60. }
  61. case PARTICIPANT_LEFT:
  62. if (store.getState()['features/follow-me'].moderator === action.participant.id) {
  63. store.dispatch(setFollowMeModerator());
  64. }
  65. break;
  66. }
  67. return next(action);
  68. });
  69. /**
  70. * Notifies this instance about a "Follow Me" command received by the Jitsi
  71. * conference.
  72. *
  73. * @param {Object} attributes - The attributes carried by the command.
  74. * @param {string} id - The identifier of the participant who issuing the
  75. * command. A notable idiosyncrasy to be mindful of here is that the command
  76. * may be issued by the local participant.
  77. * @param {Object} store - The redux store. Used to calculate and dispatch
  78. * updates.
  79. * @private
  80. * @returns {void}
  81. */
  82. function _onFollowMeCommand(attributes = {}, id, store) {
  83. const state = store.getState();
  84. // We require to know who issued the command because (1) only a
  85. // moderator is allowed to send commands and (2) a command MUST be
  86. // issued by a defined commander.
  87. if (typeof id === 'undefined') {
  88. return;
  89. }
  90. const participantSendingCommand = getParticipantById(state, id);
  91. // The Command(s) API will send us our own commands and we don't want
  92. // to act upon them.
  93. if (participantSendingCommand.local) {
  94. return;
  95. }
  96. if (participantSendingCommand.role !== 'moderator') {
  97. logger.warn('Received follow-me command not from moderator');
  98. return;
  99. }
  100. if (!isFollowMeActive(state)) {
  101. store.dispatch(setFollowMeModerator(id));
  102. }
  103. // just a command that follow me was turned off
  104. if (attributes.off) {
  105. store.dispatch(setFollowMeModerator());
  106. return;
  107. }
  108. const oldState = state['features/follow-me'].state || {};
  109. store.dispatch(setFollowMeState(attributes));
  110. // XMPP will translate all booleans to strings, so explicitly check against
  111. // the string form of the boolean {@code true}.
  112. if (oldState.filmstripVisible !== attributes.filmstripVisible) {
  113. store.dispatch(setFilmstripVisible(attributes.filmstripVisible === 'true'));
  114. }
  115. if (oldState.tileViewEnabled !== attributes.tileViewEnabled) {
  116. store.dispatch(setTileView(attributes.tileViewEnabled === 'true'));
  117. }
  118. // For now gate etherpad checks behind a web-app check to be extra safe
  119. // against calling a web-app global.
  120. if (typeof APP !== 'undefined'
  121. && oldState.sharedDocumentVisible !== attributes.sharedDocumentVisible) {
  122. const isEtherpadVisible = attributes.sharedDocumentVisible === 'true';
  123. const documentManager = APP.UI.getSharedDocumentManager();
  124. if (documentManager
  125. && isEtherpadVisible !== state['features/etherpad'].editing) {
  126. documentManager.toggleEtherpad();
  127. }
  128. }
  129. const pinnedParticipant = getPinnedParticipant(state);
  130. const idOfParticipantToPin = attributes.nextOnStage;
  131. if (typeof idOfParticipantToPin !== 'undefined'
  132. && (!pinnedParticipant || idOfParticipantToPin !== pinnedParticipant.id)
  133. && oldState.nextOnStage !== attributes.nextOnStage) {
  134. _pinVideoThumbnailById(store, idOfParticipantToPin);
  135. } else if (typeof idOfParticipantToPin === 'undefined' && pinnedParticipant) {
  136. store.dispatch(pinParticipant(null));
  137. }
  138. }
  139. /**
  140. * Pins the video thumbnail given by clickId.
  141. *
  142. * @param {Object} store - The redux store.
  143. * @param {string} clickId - The identifier of the participant to pin.
  144. * @private
  145. * @returns {void}
  146. */
  147. function _pinVideoThumbnailById(store, clickId) {
  148. if (getParticipantById(store.getState(), clickId)) {
  149. clearTimeout(nextOnStageTimeout);
  150. nextOnStageTimer = 0;
  151. store.dispatch(pinParticipant(clickId));
  152. } else {
  153. nextOnStageTimeout = setTimeout(() => {
  154. if (nextOnStageTimer > _FOLLOW_ME_RECEIVED_TIMEOUT) {
  155. nextOnStageTimer = 0;
  156. return;
  157. }
  158. nextOnStageTimer++;
  159. _pinVideoThumbnailById(store, clickId);
  160. }, 1000);
  161. }
  162. }