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.

MeetingParticipantContextMenu.js 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. // @flow
  2. import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
  3. import { useTranslation } from 'react-i18next';
  4. import { useDispatch, useSelector } from 'react-redux';
  5. import { openDialog } from '../../base/dialog';
  6. import {
  7. IconCloseCircle,
  8. IconCrown,
  9. IconMessage,
  10. IconMuteEveryoneElse,
  11. IconVideoOff
  12. } from '../../base/icons';
  13. import { isLocalParticipantModerator } from '../../base/participants';
  14. import { getIsParticipantVideoMuted } from '../../base/tracks';
  15. import { openChat } from '../../chat/actions';
  16. import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../video-menu';
  17. import MuteRemoteParticipantsVideoDialog from '../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
  18. import { getComputedOuterHeight } from '../functions';
  19. import {
  20. ContextMenu,
  21. ContextMenuIcon,
  22. ContextMenuItem,
  23. ContextMenuItemGroup,
  24. ignoredChildClassName
  25. } from './styled';
  26. type Props = {
  27. /**
  28. * Target elements against which positioning calculations are made
  29. */
  30. offsetTarget: HTMLElement,
  31. /**
  32. * Callback for the mouse entering the component
  33. */
  34. onEnter: Function,
  35. /**
  36. * Callback for the mouse leaving the component
  37. */
  38. onLeave: Function,
  39. /**
  40. * Callback for making a selection in the menu
  41. */
  42. onSelect: Function,
  43. /**
  44. * Participant reference
  45. */
  46. participant: Object
  47. };
  48. export const MeetingParticipantContextMenu = ({
  49. offsetTarget,
  50. onEnter,
  51. onLeave,
  52. onSelect,
  53. participant
  54. }: Props) => {
  55. const dispatch = useDispatch();
  56. const containerRef = useRef(null);
  57. const isLocalModerator = useSelector(isLocalParticipantModerator);
  58. const isParticipantVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
  59. const [ isHidden, setIsHidden ] = useState(true);
  60. const { t } = useTranslation();
  61. useLayoutEffect(() => {
  62. if (participant
  63. && containerRef.current
  64. && offsetTarget?.offsetParent
  65. && offsetTarget.offsetParent instanceof HTMLElement
  66. ) {
  67. const { current: container } = containerRef;
  68. const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
  69. const outerHeight = getComputedOuterHeight(container);
  70. container.style.top = offsetTop + outerHeight > offsetHeight + scrollTop
  71. ? offsetTop - outerHeight
  72. : offsetTop;
  73. setIsHidden(false);
  74. } else {
  75. setIsHidden(true);
  76. }
  77. }, [ participant, offsetTarget ]);
  78. const grantModerator = useCallback(() => {
  79. dispatch(openDialog(GrantModeratorDialog, {
  80. participantID: participant.id
  81. }));
  82. }, [ dispatch, participant ]);
  83. const kick = useCallback(() => {
  84. dispatch(openDialog(KickRemoteParticipantDialog, {
  85. participantID: participant.id
  86. }));
  87. }, [ dispatch, participant ]);
  88. const muteEveryoneElse = useCallback(() => {
  89. dispatch(openDialog(MuteEveryoneDialog, {
  90. exclude: [ participant.id ]
  91. }));
  92. }, [ dispatch, participant ]);
  93. const muteVideo = useCallback(() => {
  94. dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
  95. participantID: participant.id
  96. }));
  97. }, [ dispatch, participant ]);
  98. const sendPrivateMessage = useCallback(() => {
  99. dispatch(openChat(participant));
  100. }, [ dispatch, participant ]);
  101. if (!participant) {
  102. return null;
  103. }
  104. return (
  105. <ContextMenu
  106. className = { ignoredChildClassName }
  107. innerRef = { containerRef }
  108. isHidden = { isHidden }
  109. onClick = { onSelect }
  110. onMouseEnter = { onEnter }
  111. onMouseLeave = { onLeave }>
  112. <ContextMenuItemGroup>
  113. {isLocalModerator && (
  114. <ContextMenuItem onClick = { muteEveryoneElse }>
  115. <ContextMenuIcon src = { IconMuteEveryoneElse } />
  116. <span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
  117. </ContextMenuItem>
  118. )}
  119. {isLocalModerator && (isParticipantVideoMuted || (
  120. <ContextMenuItem onClick = { muteVideo }>
  121. <ContextMenuIcon src = { IconVideoOff } />
  122. <span>{t('participantsPane.actions.stopVideo')}</span>
  123. </ContextMenuItem>
  124. ))}
  125. </ContextMenuItemGroup>
  126. <ContextMenuItemGroup>
  127. {isLocalModerator && (
  128. <ContextMenuItem onClick = { grantModerator }>
  129. <ContextMenuIcon src = { IconCrown } />
  130. <span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
  131. </ContextMenuItem>
  132. )}
  133. {isLocalModerator && (
  134. <ContextMenuItem onClick = { kick }>
  135. <ContextMenuIcon src = { IconCloseCircle } />
  136. <span>{t('videothumbnail.kick')}</span>
  137. </ContextMenuItem>
  138. )}
  139. <ContextMenuItem onClick = { sendPrivateMessage }>
  140. <ContextMenuIcon src = { IconMessage } />
  141. <span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
  142. </ContextMenuItem>
  143. </ContextMenuItemGroup>
  144. </ContextMenu>
  145. );
  146. };