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 4.9KB

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