Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

functions.ts 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. /* eslint-disable lines-around-comment */
  2. // @ts-ignore
  3. import { getGravatarURL } from '@jitsi/js-utils/avatar';
  4. import { IStore } from '../../app/types';
  5. // @ts-ignore
  6. import { isStageFilmstripAvailable } from '../../filmstrip/functions';
  7. import { IStateful } from '../app/types';
  8. import { GRAVATAR_BASE_URL } from '../avatar/constants';
  9. import { isCORSAvatarURL } from '../avatar/functions';
  10. import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config/functions.any';
  11. import i18next from '../i18n/i18next';
  12. import { JitsiParticipantConnectionStatus, JitsiTrackStreamingStatus } from '../lib-jitsi-meet';
  13. import { shouldRenderVideoTrack } from '../media/functions';
  14. import { toState } from '../redux/functions';
  15. import { getScreenShareTrack, getVideoTrackByParticipant } from '../tracks/functions';
  16. import { createDeferred } from '../util/helpers';
  17. import {
  18. JIGASI_PARTICIPANT_ICON,
  19. MAX_DISPLAY_NAME_LENGTH,
  20. PARTICIPANT_ROLE,
  21. WHITEBOARD_PARTICIPANT_ICON
  22. } from './constants';
  23. // @ts-ignore
  24. import { preloadImage } from './preloadImage';
  25. import { Participant } from './types';
  26. /**
  27. * Temp structures for avatar urls to be checked/preloaded.
  28. */
  29. const AVATAR_QUEUE: Object[] = [];
  30. const AVATAR_CHECKED_URLS = new Map();
  31. /* eslint-disable arrow-body-style, no-unused-vars */
  32. const AVATAR_CHECKER_FUNCTIONS = [
  33. (participant: Participant) => {
  34. return participant?.isJigasi ? JIGASI_PARTICIPANT_ICON : null;
  35. },
  36. (participant: Participant) => {
  37. return participant?.isWhiteboard ? WHITEBOARD_PARTICIPANT_ICON : null;
  38. },
  39. (participant: Participant) => {
  40. return participant?.avatarURL ? participant.avatarURL : null;
  41. },
  42. (participant: Participant, store: IStore) => {
  43. const config = store.getState()['features/base/config'];
  44. const isGravatarDisabled = config.gravatar?.disabled;
  45. if (participant?.email && !isGravatarDisabled) {
  46. const gravatarBaseURL = config.gravatar?.baseUrl
  47. || config.gravatarBaseURL
  48. || GRAVATAR_BASE_URL;
  49. return getGravatarURL(participant.email, gravatarBaseURL);
  50. }
  51. return null;
  52. }
  53. ];
  54. /* eslint-enable arrow-body-style, no-unused-vars */
  55. /**
  56. * Returns the list of active speakers that should be moved to the top of the sorted list of participants so that the
  57. * dominant speaker is visible always on the vertical filmstrip in stage layout.
  58. *
  59. * @param {Function | Object} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
  60. * retrieve the state.
  61. * @returns {Array<string>}
  62. */
  63. export function getActiveSpeakersToBeDisplayed(stateful: IStateful) {
  64. const state = toState(stateful);
  65. const {
  66. dominantSpeaker,
  67. fakeParticipants,
  68. sortedRemoteScreenshares,
  69. sortedRemoteVirtualScreenshareParticipants,
  70. speakersList
  71. } = state['features/base/participants'];
  72. const { visibleRemoteParticipants } = state['features/filmstrip'];
  73. let activeSpeakers = new Map(speakersList);
  74. // Do not re-sort the active speakers if dominant speaker is currently visible.
  75. if (dominantSpeaker && visibleRemoteParticipants.has(dominantSpeaker)) {
  76. return activeSpeakers;
  77. }
  78. let availableSlotsForActiveSpeakers = visibleRemoteParticipants.size;
  79. if (activeSpeakers.has(dominantSpeaker ?? '')) {
  80. activeSpeakers.delete(dominantSpeaker ?? '');
  81. }
  82. // Add dominant speaker to the beginning of the list (not including self) since the active speaker list is always
  83. // alphabetically sorted.
  84. if (dominantSpeaker && dominantSpeaker !== getLocalParticipant(state)?.id) {
  85. const updatedSpeakers = Array.from(activeSpeakers);
  86. updatedSpeakers.splice(0, 0, [ dominantSpeaker, getParticipantById(state, dominantSpeaker)?.name ?? '' ]);
  87. activeSpeakers = new Map(updatedSpeakers);
  88. }
  89. // Remove screenshares from the count.
  90. if (getMultipleVideoSupportFeatureFlag(state)) {
  91. if (sortedRemoteVirtualScreenshareParticipants) {
  92. availableSlotsForActiveSpeakers -= sortedRemoteVirtualScreenshareParticipants.size * 2;
  93. for (const screenshare of Array.from(sortedRemoteVirtualScreenshareParticipants.keys())) {
  94. const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare as string);
  95. activeSpeakers.delete(ownerId);
  96. }
  97. }
  98. } else if (sortedRemoteScreenshares) {
  99. availableSlotsForActiveSpeakers -= sortedRemoteScreenshares.size;
  100. for (const id of Array.from(sortedRemoteScreenshares.keys())) {
  101. activeSpeakers.delete(id);
  102. }
  103. }
  104. // Remove shared video from the count.
  105. if (fakeParticipants) {
  106. availableSlotsForActiveSpeakers -= fakeParticipants.size;
  107. }
  108. const truncatedSpeakersList = Array.from(activeSpeakers).slice(0, availableSlotsForActiveSpeakers);
  109. truncatedSpeakersList.sort((a: any, b: any) => a[1].localeCompare(b[1]));
  110. return new Map(truncatedSpeakersList);
  111. }
  112. /**
  113. * Resolves the first loadable avatar URL for a participant.
  114. *
  115. * @param {Object} participant - The participant to resolve avatars for.
  116. * @param {Store} store - Redux store.
  117. * @returns {Promise}
  118. */
  119. export function getFirstLoadableAvatarUrl(participant: Participant, store: IStore) {
  120. const deferred: any = createDeferred();
  121. const fullPromise = deferred.promise
  122. .then(() => _getFirstLoadableAvatarUrl(participant, store))
  123. .then((result: any) => {
  124. if (AVATAR_QUEUE.length) {
  125. const next: any = AVATAR_QUEUE.shift();
  126. next.resolve();
  127. }
  128. return result;
  129. });
  130. if (AVATAR_QUEUE.length) {
  131. AVATAR_QUEUE.push(deferred);
  132. } else {
  133. deferred.resolve();
  134. }
  135. return fullPromise;
  136. }
  137. /**
  138. * Returns local participant from Redux state.
  139. *
  140. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  141. * {@code getState} function to be used to retrieve the state
  142. * features/base/participants.
  143. * @returns {(Participant|undefined)}
  144. */
  145. export function getLocalParticipant(stateful: IStateful) {
  146. const state = toState(stateful)['features/base/participants'];
  147. return state.local;
  148. }
  149. /**
  150. * Returns local screen share participant from Redux state.
  151. *
  152. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  153. * {@code getState} function to be used to retrieve the state features/base/participants.
  154. * @returns {(Participant|undefined)}
  155. */
  156. export function getLocalScreenShareParticipant(stateful: IStateful) {
  157. const state = toState(stateful)['features/base/participants'];
  158. return state.localScreenShare;
  159. }
  160. /**
  161. * Returns screenshare participant.
  162. *
  163. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
  164. * retrieve the state features/base/participants.
  165. * @param {string} id - The owner ID of the screenshare participant to retrieve.
  166. * @returns {(Participant|undefined)}
  167. */
  168. export function getVirtualScreenshareParticipantByOwnerId(stateful: IStateful, id: string) {
  169. const state = toState(stateful);
  170. if (getMultipleVideoSupportFeatureFlag(state)) {
  171. const track = getScreenShareTrack(state['features/base/tracks'], id);
  172. return getParticipantById(stateful, track?.jitsiTrack.getSourceName());
  173. }
  174. return;
  175. }
  176. /**
  177. * Normalizes a display name so then no invalid values (padding, length...etc)
  178. * can be set.
  179. *
  180. * @param {string} name - The display name to set.
  181. * @returns {string}
  182. */
  183. export function getNormalizedDisplayName(name: string) {
  184. if (!name || !name.trim()) {
  185. return undefined;
  186. }
  187. return name.trim().substring(0, MAX_DISPLAY_NAME_LENGTH);
  188. }
  189. /**
  190. * Returns participant by ID from Redux state.
  191. *
  192. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  193. * {@code getState} function to be used to retrieve the state
  194. * features/base/participants.
  195. * @param {string} id - The ID of the participant to retrieve.
  196. * @private
  197. * @returns {(Participant|undefined)}
  198. */
  199. export function getParticipantById(stateful: IStateful, id: string): Participant | undefined {
  200. const state = toState(stateful)['features/base/participants'];
  201. const { local, localScreenShare, remote } = state;
  202. return remote.get(id)
  203. || (local?.id === id ? local : undefined)
  204. || (localScreenShare?.id === id ? localScreenShare : undefined);
  205. }
  206. /**
  207. * Returns the participant with the ID matching the passed ID or the local participant if the ID is
  208. * undefined.
  209. *
  210. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  211. * {@code getState} function to be used to retrieve the state
  212. * features/base/participants.
  213. * @param {string|undefined} [participantID] - An optional partipantID argument.
  214. * @returns {Participant|undefined}
  215. */
  216. export function getParticipantByIdOrUndefined(stateful: IStateful, participantID?: string) {
  217. return participantID ? getParticipantById(stateful, participantID) : getLocalParticipant(stateful);
  218. }
  219. /**
  220. * Returns a count of the known participants in the passed in redux state,
  221. * excluding any fake participants.
  222. *
  223. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  224. * {@code getState} function to be used to retrieve the state
  225. * features/base/participants.
  226. * @returns {number}
  227. */
  228. export function getParticipantCount(stateful: IStateful) {
  229. const state = toState(stateful);
  230. const {
  231. local,
  232. remote,
  233. fakeParticipants,
  234. sortedRemoteVirtualScreenshareParticipants
  235. } = state['features/base/participants'];
  236. if (getMultipleVideoSupportFeatureFlag(state)) {
  237. return remote.size - fakeParticipants.size - sortedRemoteVirtualScreenshareParticipants.size + (local ? 1 : 0);
  238. }
  239. return remote.size - fakeParticipants.size + (local ? 1 : 0);
  240. }
  241. /**
  242. * Returns participant ID of the owner of a virtual screenshare participant.
  243. *
  244. * @param {string} id - The ID of the virtual screenshare participant.
  245. * @private
  246. * @returns {(string|undefined)}
  247. */
  248. export function getVirtualScreenshareParticipantOwnerId(id: string) {
  249. return id.split('-')[0];
  250. }
  251. /**
  252. * Returns the Map with fake participants.
  253. *
  254. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  255. * {@code getState} function to be used to retrieve the state
  256. * features/base/participants.
  257. * @returns {Map<string, Participant>} - The Map with fake participants.
  258. */
  259. export function getFakeParticipants(stateful: IStateful) {
  260. return toState(stateful)['features/base/participants'].fakeParticipants;
  261. }
  262. /**
  263. * Returns a count of the known remote participants in the passed in redux state.
  264. *
  265. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  266. * {@code getState} function to be used to retrieve the state
  267. * features/base/participants.
  268. * @returns {number}
  269. */
  270. export function getRemoteParticipantCount(stateful: IStateful) {
  271. const state = toState(stateful);
  272. const participantsState = state['features/base/participants'];
  273. if (getMultipleVideoSupportFeatureFlag(state)) {
  274. return participantsState.remote.size - participantsState.sortedRemoteVirtualScreenshareParticipants.size;
  275. }
  276. return participantsState.remote.size;
  277. }
  278. /**
  279. * Returns a count of the known participants in the passed in redux state,
  280. * including fake participants.
  281. *
  282. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  283. * {@code getState} function to be used to retrieve the state
  284. * features/base/participants.
  285. * @returns {number}
  286. */
  287. export function getParticipantCountWithFake(stateful: IStateful) {
  288. const state = toState(stateful);
  289. const { local, localScreenShare, remote } = state['features/base/participants'];
  290. if (getMultipleVideoSupportFeatureFlag(state)) {
  291. return remote.size + (local ? 1 : 0) + (localScreenShare ? 1 : 0);
  292. }
  293. return remote.size + (local ? 1 : 0);
  294. }
  295. /**
  296. * Returns participant's display name.
  297. *
  298. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
  299. * retrieve the state.
  300. * @param {string} id - The ID of the participant's display name to retrieve.
  301. * @returns {string}
  302. */
  303. export function getParticipantDisplayName(stateful: IStateful, id: string): string {
  304. const participant = getParticipantById(stateful, id);
  305. const {
  306. defaultLocalDisplayName,
  307. defaultRemoteDisplayName
  308. } = toState(stateful)['features/base/config'];
  309. if (participant) {
  310. if (participant.isVirtualScreenshareParticipant) {
  311. return getScreenshareParticipantDisplayName(stateful, id);
  312. }
  313. if (participant.name) {
  314. return participant.name;
  315. }
  316. if (participant.local) {
  317. return defaultLocalDisplayName ?? '';
  318. }
  319. }
  320. return defaultRemoteDisplayName ?? '';
  321. }
  322. /**
  323. * Returns screenshare participant's display name.
  324. *
  325. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
  326. * retrieve the state.
  327. * @param {string} id - The ID of the screenshare participant's display name to retrieve.
  328. * @returns {string}
  329. */
  330. export function getScreenshareParticipantDisplayName(stateful: IStateful, id: string) {
  331. const ownerDisplayName = getParticipantDisplayName(stateful, getVirtualScreenshareParticipantOwnerId(id));
  332. return i18next.t('screenshareDisplayName', { name: ownerDisplayName });
  333. }
  334. /**
  335. * Returns the presence status of a participant associated with the passed id.
  336. *
  337. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  338. * {@code getState} function to be used to retrieve the state.
  339. * @param {string} id - The id of the participant.
  340. * @returns {string} - The presence status.
  341. */
  342. export function getParticipantPresenceStatus(stateful: IStateful, id: string) {
  343. if (!id) {
  344. return undefined;
  345. }
  346. const participantById = getParticipantById(stateful, id);
  347. if (!participantById) {
  348. return undefined;
  349. }
  350. return participantById.presence;
  351. }
  352. /**
  353. * Selectors for getting all remote participants.
  354. *
  355. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  356. * {@code getState} function to be used to retrieve the state
  357. * features/base/participants.
  358. * @returns {Map<string, Object>}
  359. */
  360. export function getRemoteParticipants(stateful: IStateful): Map<string, Participant> {
  361. return toState(stateful)['features/base/participants'].remote;
  362. }
  363. /**
  364. * Selectors for the getting the remote participants in the order that they are displayed in the filmstrip.
  365. *
  366. @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
  367. * retrieve the state features/filmstrip.
  368. * @returns {Array<string>}
  369. */
  370. export function getRemoteParticipantsSorted(stateful: IStateful) {
  371. return toState(stateful)['features/filmstrip'].remoteParticipants;
  372. }
  373. /**
  374. * Returns the participant which has its pinned state set to truthy.
  375. *
  376. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  377. * {@code getState} function to be used to retrieve the state
  378. * features/base/participants.
  379. * @returns {(Participant|undefined)}
  380. */
  381. export function getPinnedParticipant(stateful: IStateful) {
  382. const state = toState(stateful);
  383. const { pinnedParticipant } = state['features/base/participants'];
  384. const stageFilmstrip = isStageFilmstripAvailable(state);
  385. if (stageFilmstrip) {
  386. const { activeParticipants } = state['features/filmstrip'];
  387. const id = activeParticipants.find(p => p.pinned)?.participantId;
  388. return id ? getParticipantById(stateful, id) : undefined;
  389. }
  390. if (!pinnedParticipant) {
  391. return undefined;
  392. }
  393. return getParticipantById(stateful, pinnedParticipant);
  394. }
  395. /**
  396. * Returns true if the participant is a moderator.
  397. *
  398. * @param {string} participant - Participant object.
  399. * @returns {boolean}
  400. */
  401. export function isParticipantModerator(participant?: Participant) {
  402. return participant?.role === PARTICIPANT_ROLE.MODERATOR;
  403. }
  404. /**
  405. * Returns the dominant speaker participant.
  406. *
  407. * @param {(Function|Object)} stateful - The (whole) redux state or redux's
  408. * {@code getState} function to be used to retrieve the state features/base/participants.
  409. * @returns {Participant} - The participant from the redux store.
  410. */
  411. export function getDominantSpeakerParticipant(stateful: IStateful) {
  412. const state = toState(stateful)['features/base/participants'];
  413. const { dominantSpeaker } = state;
  414. if (!dominantSpeaker) {
  415. return undefined;
  416. }
  417. return getParticipantById(stateful, dominantSpeaker);
  418. }
  419. /**
  420. * Returns true if all of the meeting participants are moderators.
  421. *
  422. * @param {Object|Function} stateful -Object or function that can be resolved
  423. * to the Redux state.
  424. * @returns {boolean}
  425. */
  426. export function isEveryoneModerator(stateful: IStateful) {
  427. const state = toState(stateful)['features/base/participants'];
  428. return state.everyoneIsModerator === true;
  429. }
  430. /**
  431. * Checks a value and returns true if it's a preloaded icon object.
  432. *
  433. * @param {?string | ?Object} icon - The icon to check.
  434. * @returns {boolean}
  435. */
  436. export function isIconUrl(icon?: string | Object) {
  437. return Boolean(icon) && (typeof icon === 'object' || typeof icon === 'function');
  438. }
  439. /**
  440. * Returns true if the current local participant is a moderator in the
  441. * conference.
  442. *
  443. * @param {Object|Function} stateful - Object or function that can be resolved
  444. * to the Redux state.
  445. * @returns {boolean}
  446. */
  447. export function isLocalParticipantModerator(stateful: IStateful) {
  448. const state = toState(stateful)['features/base/participants'];
  449. const { local } = state;
  450. if (!local) {
  451. return false;
  452. }
  453. return isParticipantModerator(local);
  454. }
  455. /**
  456. * Returns true if the video of the participant should be rendered.
  457. * NOTE: This is currently only used on mobile.
  458. *
  459. * @param {Object|Function} stateful - Object or function that can be resolved
  460. * to the Redux state.
  461. * @param {string} id - The ID of the participant.
  462. * @returns {boolean}
  463. */
  464. export function shouldRenderParticipantVideo(stateful: IStateful, id: string) {
  465. const state = toState(stateful);
  466. const participant = getParticipantById(state, id);
  467. if (!participant) {
  468. return false;
  469. }
  470. /* First check if we have an unmuted video track. */
  471. const videoTrack = getVideoTrackByParticipant(state['features/base/tracks'], participant);
  472. if (!shouldRenderVideoTrack(videoTrack, /* waitForVideoStarted */ false)) {
  473. return false;
  474. }
  475. /* Then check if the participant connection or track streaming status is active. */
  476. if (getSourceNameSignalingFeatureFlag(state)) {
  477. // Note that this will work only if a listener is registered for the track's TrackStreamingStatus.
  478. // The associated TrackStreamingStatusImpl instance is not created or disposed when there are zero listeners.
  479. if (videoTrack
  480. && !videoTrack.local
  481. && videoTrack.jitsiTrack?.getTrackStreamingStatus() !== JitsiTrackStreamingStatus.ACTIVE) {
  482. return false;
  483. }
  484. } else {
  485. const connectionStatus = participant.connectionStatus || JitsiParticipantConnectionStatus.ACTIVE;
  486. if (connectionStatus !== JitsiParticipantConnectionStatus.ACTIVE) {
  487. return false;
  488. }
  489. }
  490. /* Then check if audio-only mode is not active. */
  491. const audioOnly = state['features/base/audio-only'].enabled;
  492. if (!audioOnly) {
  493. return true;
  494. }
  495. /* Last, check if the participant is sharing their screen and they are on stage. */
  496. const remoteScreenShares = state['features/video-layout'].remoteScreenShares || [];
  497. const largeVideoParticipantId = state['features/large-video'].participantId;
  498. const participantIsInLargeVideoWithScreen
  499. = participant.id === largeVideoParticipantId && remoteScreenShares.includes(participant.id);
  500. return participantIsInLargeVideoWithScreen;
  501. }
  502. /**
  503. * Resolves the first loadable avatar URL for a participant.
  504. *
  505. * @param {Object} participant - The participant to resolve avatars for.
  506. * @param {Store} store - Redux store.
  507. * @returns {?string}
  508. */
  509. async function _getFirstLoadableAvatarUrl(participant: Participant, store: IStore) {
  510. for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
  511. const url = AVATAR_CHECKER_FUNCTIONS[i](participant, store);
  512. if (url !== null) {
  513. if (AVATAR_CHECKED_URLS.has(url)) {
  514. const { isLoadable, isUsingCORS } = AVATAR_CHECKED_URLS.get(url) || {};
  515. if (isLoadable) {
  516. return {
  517. isUsingCORS,
  518. src: url
  519. };
  520. }
  521. } else {
  522. try {
  523. const { corsAvatarURLs } = store.getState()['features/base/config'];
  524. const useCORS = isIconUrl(url) ? false : isCORSAvatarURL(url, corsAvatarURLs);
  525. const { isUsingCORS, src } = await preloadImage(url, useCORS);
  526. AVATAR_CHECKED_URLS.set(src, {
  527. isLoadable: true,
  528. isUsingCORS
  529. });
  530. return {
  531. isUsingCORS,
  532. src
  533. };
  534. } catch (e) {
  535. AVATAR_CHECKED_URLS.set(url, {
  536. isLoadable: false,
  537. isUsingCORS: false
  538. });
  539. }
  540. }
  541. }
  542. }
  543. return undefined;
  544. }
  545. /**
  546. * Get the participants queue with raised hands.
  547. *
  548. * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
  549. * {@code getState} function to be used to retrieve the state
  550. * features/base/participants.
  551. * @returns {Array<Object>}
  552. */
  553. export function getRaiseHandsQueue(stateful: IStateful): Array<{ id: string; raisedHandTimestamp: number; }> {
  554. const { raisedHandsQueue } = toState(stateful)['features/base/participants'];
  555. return raisedHandsQueue;
  556. }
  557. /**
  558. * Returns whether the given participant has his hand raised or not.
  559. *
  560. * @param {Object} participant - The participant.
  561. * @returns {boolean} - Whether participant has raise hand or not.
  562. */
  563. export function hasRaisedHand(participant?: Participant): boolean {
  564. return Boolean(participant?.raisedHandTimestamp);
  565. }