From 91437c50e3c3c2b3eeeaed36634b7383c43e3c7d Mon Sep 17 00:00:00 2001 From: Robert Pintilii Date: Wed, 15 Dec 2021 15:18:41 +0200 Subject: [PATCH] feat(thumbnail) Video thumbnails redesign and refactor (#10351) Update video thumbnail design Update design of indicators In filmstrip view move Screen Sharing indicator to the top Removed dominant speaker indicator Use ContextMenu component for the connection stats popover Combine Remove video menu and Meeting participant context menu into one component Moved some styles from SCSS to JSS Fix mobile avatars too big Fix mobile horizontal scroll Created button for Send to breakout room action --- css/_atlaskit_overrides.scss | 8 - css/_drawer.scss | 9 - css/_popover.scss | 5 - css/_popup_menu.scss | 124 +--- css/_variables.scss | 12 - css/_videolayout_default.scss | 381 +---------- css/filmstrip/_small_video.scss | 29 +- css/filmstrip/_tile_view.scss | 11 - css/filmstrip/_tile_view_overrides.scss | 12 - .../_vertical_filmstrip_overrides.scss | 66 -- css/themes/_light.scss | 4 - interface_config.js | 2 +- lang/main.json | 4 +- .../components/context-menu/ContextMenu.js | 22 +- .../context-menu/ContextMenuItem.js | 129 ++++ .../context-menu/ContextMenuItemGroup.js | 94 +-- react/features/base/icons/svg/crown.svg | 4 +- .../base/icons/svg/mute-everyone-else.svg | 13 +- .../features/base/icons/svg/share-desktop.svg | 4 +- .../base/popover/components/Popover.web.js | 16 +- .../react/components/web/BaseIndicator.js | 107 ++- .../components/web/ConnectionIndicator.js | 125 ++-- .../components/ConnectionStatsTable.js | 48 +- .../components/web/DisplayName.js | 75 ++- .../components/web/AudioMutedIndicator.js | 34 +- .../web/DominantSpeakerIndicator.js | 51 -- .../components/web/ModeratorIndicator.js | 33 +- .../components/web/RaisedHandIndicator.js | 74 +- .../components/web/ScreenShareIndicator.js | 3 +- .../components/web/StatusIndicators.js | 48 +- .../filmstrip/components/web/Thumbnail.js | 635 ++++++------------ .../components/web/ThumbnailAudioIndicator.js | 47 ++ .../web/ThumbnailBottomIndicators.js | 84 +++ .../components/web/ThumbnailTopIndicators.js | 132 ++++ .../components/web/ThumbnailWrapper.js | 14 +- .../components/web/VideoMenuTriggerButton.js | 69 ++ .../components/web/VideoMutedIndicator.js | 43 -- .../filmstrip/components/web/index.js | 2 - react/features/filmstrip/constants.js | 79 +-- react/features/filmstrip/functions.web.js | 70 +- .../web/MeetingParticipantContextMenu.js | 462 +------------ .../components/web/RaisedHandIndicator.js | 2 +- react/features/participants-pane/constants.js | 2 + .../components/MuteEveryonesVideoButton.js | 2 +- .../components/AbstractMuteButton.js | 4 +- .../AbstractMuteEveryoneElsesVideoButton.js | 2 +- .../components/web/AskToUnmuteButton.js | 49 ++ .../components/web/ConnectionStatusButton.js | 15 +- .../components/web/FlipLocalVideoButton.js | 19 +- .../components/web/GrantModeratorButton.js | 15 +- .../components/web/HideSelfViewVideoButton.js | 21 +- .../video-menu/components/web/KickButton.js | 16 +- .../web/LocalVideoMenuTriggerButton.js | 90 ++- .../video-menu/components/web/MuteButton.js | 29 +- .../components/web/MuteEveryoneElseButton.js | 14 +- .../web/MuteEveryoneElsesVideoButton.js | 14 +- .../components/web/MuteVideoButton.js | 30 +- .../components/web/ParticipantContextMenu.js | 332 +++++++++ .../web/PrivateMessageMenuButton.js | 13 +- .../components/web/RemoteControlButton.js | 19 +- .../web/RemoteVideoMenuTriggerButton.js | 261 ++----- .../components/web/SendToRoomButton.js | 50 ++ .../video-menu/components/web/VideoMenu.js | 51 -- .../components/web/VideoMenuButton.js | 111 --- .../video-menu/components/web/VolumeSlider.js | 99 ++- .../video-menu/components/web/index.js | 2 +- 66 files changed, 1875 insertions(+), 2571 deletions(-) create mode 100644 react/features/base/components/context-menu/ContextMenuItem.js mode change 100755 => 100644 react/features/base/icons/svg/share-desktop.svg delete mode 100644 react/features/filmstrip/components/web/DominantSpeakerIndicator.js create mode 100644 react/features/filmstrip/components/web/ThumbnailAudioIndicator.js create mode 100644 react/features/filmstrip/components/web/ThumbnailBottomIndicators.js create mode 100644 react/features/filmstrip/components/web/ThumbnailTopIndicators.js create mode 100644 react/features/filmstrip/components/web/VideoMenuTriggerButton.js delete mode 100644 react/features/filmstrip/components/web/VideoMutedIndicator.js create mode 100644 react/features/video-menu/components/web/AskToUnmuteButton.js create mode 100644 react/features/video-menu/components/web/ParticipantContextMenu.js create mode 100644 react/features/video-menu/components/web/SendToRoomButton.js delete mode 100644 react/features/video-menu/components/web/VideoMenu.js delete mode 100644 react/features/video-menu/components/web/VideoMenuButton.js diff --git a/css/_atlaskit_overrides.scss b/css/_atlaskit_overrides.scss index 992eb5449..ba5f5dba0 100644 --- a/css/_atlaskit_overrides.scss +++ b/css/_atlaskit_overrides.scss @@ -24,14 +24,6 @@ bottom: calc(#{$newToolbarSizeWithPadding}) !important; } -/** - * Override @atlaskit/theme styling for the top toolbar so it displays over - * the video thumbnail while obscuring as little as possible. - */ -.videocontainer__toptoolbar > div > div { - background: none; -} - /** * Keep overflow menu within screen vertical bounds and make it scrollable. diff --git a/css/_drawer.scss b/css/_drawer.scss index c4eaf8186..981826494 100644 --- a/css/_drawer.scss +++ b/css/_drawer.scss @@ -42,15 +42,6 @@ } } - .popupmenu { - margin: auto; - width: 100%; - } - - .popupmenu__item { - height: 48px; - } - &#{&} .overflow-menu { margin: auto; font-size: 1.2em; diff --git a/css/_popover.scss b/css/_popover.scss index 6e82fda94..ff9aed51f 100644 --- a/css/_popover.scss +++ b/css/_popover.scss @@ -43,10 +43,5 @@ .popover { margin: -16px -24px; - padding: 16px 24px; z-index: $popoverZ; } - -.padded-content { - padding: 4px 8px; -} diff --git a/css/_popup_menu.scss b/css/_popup_menu.scss index 5a2a7fdbc..cd319c0d6 100644 --- a/css/_popup_menu.scss +++ b/css/_popup_menu.scss @@ -2,122 +2,18 @@ * Initialize **/ -.popupmenu { - background-color: $menuBG; - border-radius: 3px; - list-style-type: none; - min-width: 150px; - text-align: left; - padding: 0px; - white-space: nowrap; +.popupmenu__contents { + .popupmenu__volume-slider { + &::-webkit-slider-runnable-track { + background-color: $popupSliderColor; + } - &__item { - list-style-type: none; - height: 35px; - } + &::-moz-range-track { + background-color: $popupSliderColor; + } - // Link Appearance - &__link, - &__contents { - display: block; - box-sizing: border-box; - text-decoration: none; - height: 100%; - font-size: 9pt; - width: 100%; - cursor: pointer; - padding: 0 5px; - color: $popupMenuColor; - - &:hover { - background-color: $popupMenuHoverBackground; - color: $popupMenuHoverColor; - } - - &.disabled { - pointer-events: none; - } - } - - &__list { - margin: 0; - padding: 0; - } - - &__text { - display: inline-block; - margin-left: 8px; - vertical-align: middle; - } - - &__link { - i { - cursor: pointer; - } - } - - &__contents { - display: flex; - - /** - * Positioning styles on the slider and its container are used to make - * the container fit the popup width, by removing the slider from the - * page flow, and then making the slider fit the container. - */ - .popupmenu__slider_container { - position: relative; - width: 100%; - - .popupmenu__slider { - position: absolute; - top: 50%; - transform: translate(0, -50%); - width: 100%; - - &::-webkit-slider-runnable-track { - background-color: $popupSliderColor; - } - - &::-moz-range-track { - background-color: $popupSliderColor; - } - - &::-ms-fill-lower { - background-color: $popupSliderColor; - } + &::-ms-fill-lower { + background-color: $popupSliderColor; } } - } - - &__icon { - vertical-align: middle; - position: relative; - display: inline-block; - min-width: 20px; - height: 100%; - padding-right: 10px; - - > * { - @include absoluteAligning(); - } - } - - .icon-kick, - .icon-play, - .icon-stop { - font-size: 8pt; - } -} - -/** - * Override reset css styling modifying all lists and set negative margin to - * reduce the visibility of padding on AtlasKit - * InlineDialogs. - */ -ul.popupmenu { - margin: -16px -24px; -} - -span.localvideomenu:hover ul.popupmenu, span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover { - display:block !important; } diff --git a/css/_variables.scss b/css/_variables.scss index fd1d0aba9..84ea030e9 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -13,9 +13,6 @@ $hangupFontSize: 2em; */ // Video layout. -$thumbnailToolbarHeight: 22px; -$thumbnailIndicatorBorder: 2px; -$thumbnailIndicatorSize: $thumbnailToolbarHeight; $thumbnailVideoMargin: 2px; $thumbnailsBorder: 2px; $thumbnailVideoBorder: 2px; @@ -56,19 +53,12 @@ $overflowMenuItemBackground: #36383C; /** * Video layout */ -$videoThumbnailHovered: rgba(22, 94, 204, .4); -$videoThumbnailSelected: #165ECC; $participantNameColor: #fff; -$thumbnailPictogramColor: #fff; -$dominantSpeakerBg: #165ecc; -$raiseHandBg: #F8AE1A; $audioLevelBg: #44A5FF; -$connectionIndicatorBg: #165ecc; $audioLevelShadow: rgba(9, 36, 77, 0.9); $videoStateIndicatorColor: $defaultColor; $videoStateIndicatorBackground: $toolbarBackground; $videoStateIndicatorSize: 40px; -$remoteVideoMenuIconMargin: initial; /** * Feedback Modal @@ -102,7 +92,6 @@ $sidebarWidth: 315px; * Misc. */ $borderRadius: 4px; -$popoverMenuPadding: 13px; $happySoftwareBackground: transparent; $desktopAppDragBarHeight: 25px; $scrollHeight: 7px; @@ -118,7 +107,6 @@ $toolbarBackgroundZ: 4; $labelsZ: 5; $subtitlesZ: 7; $popoverZ: 8; -$zindex10: 10; $reloadZ: 20; $poweredByZ: 100; $ringingZ: 300; diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index d78a0f697..2fd87e309 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -43,165 +43,7 @@ .videocontainer { position: relative; text-align: center; - - &__background { - @include topLeft(); - background-color: black; - border-radius: $borderRadius; - width: 100%; - height: 100%; - } - - /** - * The toolbar of the video thumbnail. - */ - &__toolbar, - &__toptoolbar { - position: absolute; - left: 0; - pointer-events: none; - z-index: $zindex10; - width: 100%; - box-sizing: border-box; // Includes the padding in the 100% width. - - /** - * FIXME (lenny): Disabling pointer-events is a pretty big sin that - * sidesteps the problems. There are z-index wars occurring within - * videocontainer and AtlasKit Tooltips rely on their parent z-indexe - * being higher than whatever they need to appear over. So set a higher - * z-index for the tooltip containers but make any empty space not block - * mouse overs for various mouseover triggers. - */ - pointer-events: none; - - * { - pointer-events: auto; - } - - .indicator-container { - display: inline-block; - float: left; - pointer-events: all; - } - } - - &__toolbar { - bottom: 0; - padding: 0 5px 0 5px; - } - - &__toptoolbar { - $toolbarIconMargin: 5px; - top: 0; - padding-bottom: 0; - /** - * Override text-align center as icons need to be left justified. - */ - text-align: left; - - /** - * Intentionally use margin on the icon itself as AtlasKit InlineDialog - * positioning depends on the trigger (indicator icon). - */ - .indicator { - margin-left: 5px; - margin-top: $toolbarIconMargin; - } - - .indicator-container:nth-child(1) .indicator { - margin-left: $toolbarIconMargin; - } - - .indicator-container { - display: inline-block; - vertical-align: top; - - .popover-trigger { - display: inline-block; - } - } - - .connection-indicator, - .indicator { - position: relative; - font-size: 8px; - text-align: center; - line-height: $thumbnailIndicatorSize; - padding: 0; - @include circle($thumbnailIndicatorSize); - box-sizing: border-box; - z-index: $zindex3; - background: $dominantSpeakerBg; - color: $thumbnailPictogramColor; - border: $thumbnailIndicatorBorder solid $thumbnailPictogramColor; - - .indicatoricon { - @include absoluteAligning(); - } - - .connection { - position: relative; - display: inline-block; - margin: 0 auto; - left: 0; - @include transform(translate(0, -50%)); - - &_empty, - &_lost - { - color: #8B8B8B;/*#FFFFFF*/ - overflow: hidden; - } - - &_full - { - @include topLeft(); - color: #FFFFFF;/*#15A1ED*/ - overflow: hidden; - } - - &_ninja - { - font-size: 1.5em; - } - } - - .icon-gsm-bars { - cursor: pointer; - font-size: 1em; - } - } - - .hide-connection-indicator { - display: none; - } - } - - &__hoverOverlay { - background: rgba(0,0,0,.6); - border-radius: $borderRadius; - position: absolute; - top: 0px; - left: 0px; - width: 100%; - height: 100%; - visibility: hidden; - z-index: $zindex2; - } - - &__participant-name { - color: #fff; - background-color: rgba(0,0,0,.4); - padding: 3px 7px; - border-radius: 3px; - max-width: calc(100% - 32px); - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - height: 16px; - display: inline-block; - text-align: right; - } + overflow: 'hidden'; @media (min-width: 581px) { &.shift-right { @@ -288,16 +130,6 @@ z-index: $zindex0; } -/** - * Positions video thumbnail display name and editor. - */ -#alwaysOnTop .displayname, -.videocontainer .displayname, -.videocontainer .editdisplayname { - font-weight: 100; - color: $participantNameColor; -} - #alwaysOnTop .displayname { font-size: 15px; position: inherit; @@ -307,146 +139,6 @@ margin-top: 10px; } -/** - * Positions video thumbnail display name editor. - */ -.videocontainer .editdisplayname { - outline: none; - border: none; - background: none; - box-shadow: none; - padding: 0; -} - -#localVideoContainer .displayname:hover { - cursor: text; -} - -.videocontainer .displayname { - pointer-events: none; - padding: 0 3px 0 3px; -} - -.videocontainer .editdisplayname { - height: auto; -} - -#localDisplayName { - pointer-events: auto !important; -} - -.videocontainer>a.displayname { - display: inline-block; - position: absolute; - color: #FFFFFF; - bottom: 0; - right: 0; - padding: 3px 5px; - font-size: 9pt; - cursor: pointer; - z-index: $zindex2; -} - -/** - * Video thumbnail toolbar icon. - */ -.videocontainer .toolbar-icon { - font-size: 8pt; - text-align: center; - text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7); - color: #FFFFFF; - width: 12px; - line-height: $thumbnailToolbarHeight; - height: $thumbnailToolbarHeight; - padding: 0; - border: 0; - margin: 0px 5px 0px 0px; -} - -/** - * Toolbar icon internal i elements (font icons). - */ -.toolbar-icon>div { - height: $thumbnailToolbarHeight; - display: flex; - flex-direction: column; - justify-content: center; -} - -/** - * Toolbar icons positioned on the right. - */ -.moderator-icon { - display: inline-block; - - &.right { - float: right; - margin: 0px 0px 0px 5px; - } - - .toolbar-icon { - margin: 0; - } -} - -.raisehandindicator { - background: $raiseHandBg !important; -} - -.connection-indicator { - background: $connectionIndicatorBg; - - &.status-high { - background: green; - } - - &.status-med { - background: #FFD740; - } - - &.status-lost { - background: gray; - } - - &.status-low { - background: #BF2117; - } - - &.status-other { - background: $connectionIndicatorBg; - } - - &.status-disabled { - background: transparent; - border: none - } -} - -.local-video-menu-trigger, -.remote-video-menu-trigger, -.localvideomenu, -.remotevideomenu -{ - display: inline-block; - position: absolute; - top: 0px; - right: 0; - z-index: $zindex2; - width: 18px; - height: 18px; - color: #FFF; - font-size: 10pt; - margin-right: $remoteVideoMenuIconMargin; - - >i{ - cursor: hand; - } -} -.local-video-menu-trigger, -.remote-video-menu-trigger { - margin-top: 7px; -} - /** * Audio indicator on video thumbnails. */ @@ -623,74 +315,11 @@ display: none; } -.display-avatar-with-name { - .avatar-container { - visibility: visible; - } - - .displayNameContainer { - visibility: visible; - } - - .videocontainer__hoverOverlay { - visibility: visible; - } - - video { - visibility: hidden; - } -} - -.display-name-on-black { - .avatar-container { - visibility: hidden; - } - - .displayNameContainer { - visibility: visible; - } - - .videocontainer__hoverOverlay { - visibility: hidden; - } - - video { - opacity: 0.2; - visibility: visible; - } -} - .display-video { .avatar-container { visibility: hidden; } - .displayNameContainer { - visibility: hidden; - } - - .videocontainer__hoverOverlay { - visibility: hidden; - } - - video { - visibility: visible; - } -} - -.display-name-on-video { - .avatar-container { - visibility: hidden; - } - - .displayNameContainer { - visibility: visible; - } - - .videocontainer__hoverOverlay { - visibility: visible; - } - video { visibility: visible; } @@ -701,14 +330,6 @@ visibility: visible; } - .displayNameContainer { - visibility: hidden; - } - - .videocontainer__hoverOverlay { - visibility: hidden; - } - video { visibility: hidden; } diff --git a/css/filmstrip/_small_video.scss b/css/filmstrip/_small_video.scss index 4f7e5ede2..bffd1a50c 100644 --- a/css/filmstrip/_small_video.scss +++ b/css/filmstrip/_small_video.scss @@ -6,37 +6,10 @@ border-radius: $borderRadius; margin: 0 $thumbnailVideoMargin; - &.videoContainerFocused, &:hover { + &:hover { cursor: hand; } - /** - * Focused video thumbnail. - */ - &.videoContainerFocused { - border: $thumbnailVideoBorder solid $videoThumbnailSelected; - box-shadow: inset 0 0 3px $videoThumbnailSelected, - 0 0 3px $videoThumbnailSelected; - } - - .remotevideomenu > .icon-menu, .localvideomenu > .icon-menu { - display: none; - } - - /** - * Hovered video thumbnail. - */ - &:hover:not(.videoContainerFocused):not(.active-speaker) { - cursor: hand; - border: $thumbnailVideoBorder solid $videoThumbnailHovered; - box-shadow: inset 0 0 3px $videoThumbnailHovered, - 0 0 3px $videoThumbnailHovered; - - .remotevideomenu > .icon-menu, .localvideomenu > .icon-menu { - display: inline-block; - } - } - & > video { cursor: hand; border-radius: $borderRadius; diff --git a/css/filmstrip/_tile_view.scss b/css/filmstrip/_tile_view.scss index ed5c689f9..44bd177b9 100644 --- a/css/filmstrip/_tile_view.scss +++ b/css/filmstrip/_tile_view.scss @@ -2,13 +2,6 @@ * CSS styles that are specific to the filmstrip that shows the thumbnail tiles. */ .tile-view { - /** - * Add a border around the active speaker to make the thumbnail easier to - * see. - */ - .active-speaker { - box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px $videoThumbnailSelected; - } .remote-videos { align-items: center; @@ -134,7 +127,3 @@ } } } - -.indicator-icon-container { - display: inline-block; -} diff --git a/css/filmstrip/_tile_view_overrides.scss b/css/filmstrip/_tile_view_overrides.scss index 658f48918..0342ac640 100644 --- a/css/filmstrip/_tile_view_overrides.scss +++ b/css/filmstrip/_tile_view_overrides.scss @@ -35,16 +35,4 @@ #remotePresenceMessage { display: none !important; } - - /** - * Thumbnail popover menus can overlap other thumbnails. Setting an auto - * z-index will allow AtlasKit InlineDialog's large z-index to be - * respected and thereby display over elements in other thumbnails, - * specifically the various status icons. - */ - .remotevideomenu, - .localvideomenu, - .videocontainer__toptoolbar { - z-index: auto; - } } diff --git a/css/filmstrip/_vertical_filmstrip_overrides.scss b/css/filmstrip/_vertical_filmstrip_overrides.scss index fc30fe641..3c70a69ab 100644 --- a/css/filmstrip/_vertical_filmstrip_overrides.scss +++ b/css/filmstrip/_vertical_filmstrip_overrides.scss @@ -19,72 +19,6 @@ * Overrides for small videos in vertical filmstrip mode. */ .vertical-filmstrip .filmstrip__videos .videocontainer { - /** - * Move status icons to the bottom right of the thumbnail. - */ - .videocontainer__toolbar { - /** - * FIXME: disable pointer to allow any elements moved below to still - * be clickable. The real fix would to make sure those moved elements - * are actually part of the toolbar instead of positioning being faked. - */ - pointer-events: none; - text-align: right; - - > div { - pointer-events: none; - } - - .right { - float: none; - margin: auto; - } - - .toolbar-icon { - pointer-events: all; - } - } - - /** - * Apply hardware acceleration to prevent flickering on scroll. The - * selectors are specific to icon wrappers to prevent fixed position dialogs - * and tooltips from getting a new location context due to translate3d. - */ - .connection-indicator, - .local-video-menu-trigger, - .remote-video-menu-trigger, - .indicator-icon-container { - transform: translate3d(0, 0, 0); - } - - .indicator-icon-container { - display: inline-block; - } - - .indicator-container { - float: none; - } - - /** - * Move the remote video menu trigger to the bottom left of the video - * thumbnail. - */ - .localvideomenu, - .remotevideomenu, - .local-video-menu-trigger, - .remote-video-menu-trigger { - bottom: 0; - left: 0; - top: auto; - right: auto; - } - - .local-video-menu-trigger, - .remote-video-menu-trigger { - margin-bottom: 3px; - margin-left: $remoteVideoMenuIconMargin; - } - .self-view-mobile-portrait video { object-fit: contain; } diff --git a/css/themes/_light.scss b/css/themes/_light.scss index 49854d8f5..35b0dcef2 100644 --- a/css/themes/_light.scss +++ b/css/themes/_light.scss @@ -75,11 +75,7 @@ $errorColor: #c61600; $feedbackCancelFontColor: #333; // Popover colors -$popoverBg: initial; $popoverFontColor: #ffffff !important; -$popupMenuColor: #ffffff !important; -$popupMenuHoverColor: #ffffff !important; -$popupMenuHoverBackground: rgba(255, 255, 255, 0.1); $popupSliderColor: #0376da; // Toolbar diff --git a/interface_config.js b/interface_config.js index c3a76afb9..cf97296e5 100644 --- a/interface_config.js +++ b/interface_config.js @@ -26,7 +26,7 @@ var interfaceConfig = { CLOSE_PAGE_GUEST_HINT: false, // A html text to be shown to guests on the close page, false disables it - DEFAULT_BACKGROUND: '#474747', + DEFAULT_BACKGROUND: '#040404', DEFAULT_LOGO_URL: 'images/watermark.svg', DEFAULT_WELCOME_PAGE_LOGO_URL: 'images/watermark.svg', diff --git a/lang/main.json b/lang/main.json index c26dba26d..7335bb4e5 100644 --- a/lang/main.json +++ b/lang/main.json @@ -949,8 +949,8 @@ "mute": "Mute / Unmute", "muteEveryone": "Mute everyone", "muteEveryoneElse": "Mute everyone else", - "muteEveryonesVideo": "Disable everyone's video", - "muteEveryoneElsesVideo": "Disable everyone else's video", + "muteEveryonesVideoStream": "Stop everyone's video", + "muteEveryoneElsesVideoStream": "Stop everyone else's video", "participants": "Participants", "pip": "Toggle Picture-in-Picture mode", "privateMessage": "Send private message", diff --git a/react/features/base/components/context-menu/ContextMenu.js b/react/features/base/components/context-menu/ContextMenu.js index 0aee6d45d..bf556d706 100644 --- a/react/features/base/components/context-menu/ContextMenu.js +++ b/react/features/base/components/context-menu/ContextMenu.js @@ -19,7 +19,7 @@ type Props = { /** * Class name for context menu. Used to overwrite default styles. */ - className?: string, + className?: ?string, /** * The entity for which the context menu is displayed. @@ -31,10 +31,15 @@ type Props = { */ hidden?: boolean, + /** + * Whether or not the menu is already in a drawer. + */ + inDrawer?: ?boolean, + /** * Whether or not drawer should be open. */ - isDrawerOpen: boolean, + isDrawerOpen?: boolean, /** * Target elements against which positioning calculations are made. @@ -49,7 +54,7 @@ type Props = { /** * Callback for drawer close. */ - onDrawerClose: Function, + onDrawerClose?: Function, /** * Callback for the mouse entering the component. @@ -59,7 +64,7 @@ type Props = { /** * Callback for the mouse leaving the component. */ - onMouseLeave: Function + onMouseLeave?: Function }; const useStyles = makeStyles(theme => { @@ -106,6 +111,7 @@ const ContextMenu = ({ className, entity, hidden, + inDrawer, isDrawerOpen, offsetTarget, onClick, @@ -147,6 +153,14 @@ const ContextMenu = ({ } }, [ hidden ]); + if (_overflowDrawer && inDrawer) { + return (
+ {children} +
); + } + return _overflowDrawer ? { + return { + contextMenuItem: { + alignItems: 'center', + cursor: 'pointer', + display: 'flex', + minHeight: '40px', + padding: '10px 16px', + boxSizing: 'border-box', + + '& > *:not(:last-child)': { + marginRight: `${theme.spacing(3)}px` + }, + + '&:hover': { + backgroundColor: theme.palette.ui04 + } + }, + + contextMenuItemDisabled: { + pointerEvents: 'none' + }, + + contextMenuItemDrawer: { + padding: '12px 16px' + }, + + contextMenuItemIcon: { + '& svg': { + fill: theme.palette.icon01 + } + } + }; +}); + +const ContextMenuItem = ({ + accessibilityLabel, + className, + customIcon, + disabled, + id, + icon, + onClick, + text, + textClassName }: Props) => { + const styles = useStyles(); + const _overflowDrawer = useSelector(showOverflowDrawer); + + return ( +
+ {customIcon ? customIcon + : icon && } + {text} +
+ ); +}; + +export default ContextMenuItem; diff --git a/react/features/base/components/context-menu/ContextMenuItemGroup.js b/react/features/base/components/context-menu/ContextMenuItemGroup.js index eb6d37a09..1e59eb8ef 100644 --- a/react/features/base/components/context-menu/ContextMenuItemGroup.js +++ b/react/features/base/components/context-menu/ContextMenuItemGroup.js @@ -1,50 +1,8 @@ // @flow import { makeStyles } from '@material-ui/core'; -import clsx from 'clsx'; import React from 'react'; -import { useSelector } from 'react-redux'; -import { showOverflowDrawer } from '../../../toolbox/functions.web'; -import { Icon } from '../../icons'; - -export type Action = { - - /** - * Label used for accessibility. - */ - accessibilityLabel: string, - - /** - * CSS class name used for custom styles. - */ - className?: string, - - /** - * Custom icon. If used, the icon prop is ignored. - * Used to allow custom children instead of just the default icons. - */ - customIcon?: React$Node, - - /** - * Id of the action container. - */ - id?: string, - - /** - * Default icon for action. - */ - icon?: Function, - - /** - * Click handler. - */ - onClick?: Function, - - /** - * Action text. - */ - text: string -} +import ContextMenuItem, { type Props as Action } from './ContextMenuItem'; type Props = { @@ -59,7 +17,6 @@ type Props = { children?: React$Node, }; - const useStyles = makeStyles(theme => { return { contextMenuItemGroup: { @@ -70,33 +27,6 @@ const useStyles = makeStyles(theme => { '& + &:not(:empty)': { borderTop: `1px solid ${theme.palette.ui04}` } - }, - - contextMenuItem: { - alignItems: 'center', - cursor: 'pointer', - display: 'flex', - minHeight: '40px', - padding: '10px 16px', - boxSizing: 'border-box', - - '& > *:not(:last-child)': { - marginRight: `${theme.spacing(3)}px` - }, - - '&:hover': { - backgroundColor: theme.palette.ui04 - } - }, - - contextMenuItemDrawer: { - padding: '12px 16px' - }, - - contextMenuItemIcon: { - '& svg': { - fill: theme.palette.icon01 - } } }; }); @@ -106,28 +36,14 @@ const ContextMenuItemGroup = ({ children }: Props) => { const styles = useStyles(); - const _overflowDrawer = useSelector(showOverflowDrawer); return (
{children} - {actions && actions.map(({ accessibilityLabel, className, customIcon, id, icon, onClick, text }) => ( -
- {customIcon ? customIcon - : icon && } - {text} -
+ {actions && actions.map(actionProps => ( + ))}
); diff --git a/react/features/base/icons/svg/crown.svg b/react/features/base/icons/svg/crown.svg index c35d74923..22d7ec669 100644 --- a/react/features/base/icons/svg/crown.svg +++ b/react/features/base/icons/svg/crown.svg @@ -1,3 +1,3 @@ - - + + diff --git a/react/features/base/icons/svg/mute-everyone-else.svg b/react/features/base/icons/svg/mute-everyone-else.svg index 4c37c9161..b62881034 100644 --- a/react/features/base/icons/svg/mute-everyone-else.svg +++ b/react/features/base/icons/svg/mute-everyone-else.svg @@ -1,11 +1,4 @@ - - - - - - - - - - + + + diff --git a/react/features/base/icons/svg/share-desktop.svg b/react/features/base/icons/svg/share-desktop.svg old mode 100755 new mode 100644 index e85acaa07..164780964 --- a/react/features/base/icons/svg/share-desktop.svg +++ b/react/features/base/icons/svg/share-desktop.svg @@ -1,3 +1,3 @@ - - + + diff --git a/react/features/base/popover/components/Popover.web.js b/react/features/base/popover/components/Popover.web.js index e49b325d8..333ceed21 100644 --- a/react/features/base/popover/components/Popover.web.js +++ b/react/features/base/popover/components/Popover.web.js @@ -1,5 +1,4 @@ /* @flow */ -import clsx from 'clsx'; import React, { Component } from 'react'; import { Drawer, JitsiPortal, DialogPortal } from '../../../toolbox/components/web'; @@ -60,11 +59,6 @@ type Props = { */ position: string, - /** - * Whether the content show have some padding. - */ - paddedContent: ?boolean, - /** * Whether the popover is visible or not. */ @@ -79,7 +73,7 @@ type State = { /** * The style to apply to the context menu in order to position it correctly. */ - contextMenuStyle: Object + contextMenuStyle: Object }; /** @@ -364,15 +358,11 @@ class Popover extends Component { * @returns {ReactElement} */ _renderContent() { - const { content, paddedContent } = this.props; - const className = clsx( - 'popover popupmenu', - paddedContent && 'padded-content' - ); + const { content } = this.props; return (
{ content } {!isMobileBrowser() && ( diff --git a/react/features/base/react/components/web/BaseIndicator.js b/react/features/base/react/components/web/BaseIndicator.js index 96a50fac3..8ec741264 100644 --- a/react/features/base/react/components/web/BaseIndicator.js +++ b/react/features/base/react/components/web/BaseIndicator.js @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component } from 'react'; +import { makeStyles } from '@material-ui/core'; +import React from 'react'; import { translate } from '../../../i18n'; import { Icon } from '../../../icons'; @@ -12,7 +13,7 @@ import { Tooltip } from '../../../tooltip'; type Props = { /** - * Additional CSS class names to set on the icon container. + * Additional CSS class name. */ className: string, @@ -59,66 +60,58 @@ type Props = { tooltipPosition: string }; +const useStyles = makeStyles(() => { + return { + indicator: { + width: '20px', + height: '20px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }; +}); + /** * React {@code Component} for showing an icon with a tooltip. * - * @augments Component + * @returns {ReactElement} */ -class BaseIndicator extends Component { - /** - * Default values for {@code BaseIndicator} component's properties. - * - * @static - */ - static defaultProps = { - className: '', - id: '', - tooltipPosition: 'top' - }; +const BaseIndicator = ({ + className = '', + icon, + iconClassName, + iconId, + iconSize, + id = '', + t, + tooltipKey, + tooltipPosition = 'top' +}: Props) => { + const styles = useStyles(); + const style = {}; - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { - className, - icon, - iconClassName, - iconId, - iconSize, - id, - t, - tooltipKey, - tooltipPosition - } = this.props; - const iconContainerClassName = `indicator-icon-container ${className}`; - const style = {}; - - if (iconSize) { - style.fontSize = iconSize; - } - - return ( -
- - - - - -
- ); + if (iconSize) { + style.fontSize = iconSize; } -} + + return ( +
+ + + + + +
+ ); +}; export default translate(BaseIndicator); diff --git a/react/features/connection-indicator/components/web/ConnectionIndicator.js b/react/features/connection-indicator/components/web/ConnectionIndicator.js index 7a2756b40..14d109453 100644 --- a/react/features/connection-indicator/components/web/ConnectionIndicator.js +++ b/react/features/connection-indicator/components/web/ConnectionIndicator.js @@ -1,5 +1,7 @@ // @flow +import { withStyles } from '@material-ui/styles'; +import clsx from 'clsx'; import React from 'react'; import type { Dispatch } from 'redux'; @@ -30,24 +32,21 @@ const QUALITY_TO_WIDTH: Array = [ { colorClass: 'status-high', percent: INDICATOR_DISPLAY_THRESHOLD, - tip: 'connectionindicator.quality.good', - width: '100%' + tip: 'connectionindicator.quality.good' }, // 2 bars { colorClass: 'status-med', percent: 10, - tip: 'connectionindicator.quality.nonoptimal', - width: '66%' + tip: 'connectionindicator.quality.nonoptimal' }, // 1 bar { colorClass: 'status-low', percent: 0, - tip: 'connectionindicator.quality.poor', - width: '33%' + tip: 'connectionindicator.quality.poor' } // Note: we never show 0 bars as long as there is a connection. @@ -85,6 +84,11 @@ type Props = AbstractProps & { */ audioSsrc: number, + /** + * An object containing the CSS classes. + */ + classes: Object, + /** * The Redux dispatch function. */ @@ -122,6 +126,52 @@ type State = AbstractState & { popoverVisible: boolean } +const styles = theme => { + return { + container: { + display: 'inline-block' + }, + + hidden: { + display: 'none' + }, + + icon: { + padding: '6px', + borderRadius: '4px', + + '&.status-high': { + backgroundColor: theme.palette.success01 + }, + + '&.status-med': { + backgroundColor: theme.palette.warning01 + }, + + '&.status-low': { + backgroundColor: theme.palette.iconError + }, + + '&.status-disabled': { + background: 'transparent' + }, + + '&.status-lost': { + backgroundColor: theme.palette.ui05 + }, + + '&.status-other': { + backgroundColor: theme.palette.action01 + } + }, + + inactiveIcon: { + padding: 0, + borderRadius: '50%' + } + }; +}; + /** * Implements a React {@link Component} which displays the current connection * quality percentage and has a popover to show more detailed connection stats. @@ -154,9 +204,8 @@ class ConnectionIndicator extends AbstractConnectionIndicator { * @returns {ReactElement} */ render() { - const { enableStatsDisplay, participantId, statsPopoverPosition } = this.props; + const { enableStatsDisplay, participantId, statsPopoverPosition, classes } = this.props; const visibilityClass = this._getVisibilityClass(); - const rootClassNames = `indicator-container ${visibilityClass}`; if (this.props._popoverDisabled) { return this._renderIndicator(); @@ -164,7 +213,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator { return ( } @@ -173,7 +222,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator { noPaddingContent = { true } onPopoverClose = { this._onHidePopover } onPopoverOpen = { this._onShowPopover } - paddedContent = { true } position = { statsPopoverPosition } visible = { this.state.popoverVisible }> { this._renderIndicator() } @@ -231,13 +279,13 @@ class ConnectionIndicator extends AbstractConnectionIndicator { * @returns {string} */ _getVisibilityClass() { - const { _connectionStatus } = this.props; + const { _connectionStatus, classes } = this.props; return this.state.showIndicator || this.props.alwaysVisible || _connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED || _connectionStatus === JitsiParticipantConnectionStatus.INACTIVE - ? 'show-connection-indicator' : 'hide-connection-indicator'; + ? '' : classes.hidden; } _onHidePopover: () => void; @@ -259,6 +307,8 @@ class ConnectionIndicator extends AbstractConnectionIndicator { * @returns {ReactElement} */ _renderIcon() { + const colorClass = this._getConnectionColorClass(); + if (this.props._connectionStatus === JitsiParticipantConnectionStatus.INACTIVE) { if (this.props._connectionIndicatorInactiveDisabled) { return null; @@ -267,14 +317,13 @@ class ConnectionIndicator extends AbstractConnectionIndicator { return ( ); } - let iconWidth; let emptyIconWrapperClassName = 'connection_empty'; if (this.props._connectionStatus @@ -283,34 +332,16 @@ class ConnectionIndicator extends AbstractConnectionIndicator { // emptyIconWrapperClassName is used by the torture tests to // identify lost connection status handling. emptyIconWrapperClassName = 'connection_lost'; - iconWidth = '0%'; - } else if (typeof this.state.stats.percent === 'undefined') { - iconWidth = '100%'; - } else { - const { percent } = this.state.stats; - - iconWidth = this._getDisplayConfiguration(percent).width; } - return [ - + return ( + - , - - - ]; + ); } _onShowPopover: () => void; @@ -332,19 +363,10 @@ class ConnectionIndicator extends AbstractConnectionIndicator { * @returns {ReactElement} */ _renderIndicator() { - const colorClass = this._getConnectionColorClass(); - const indicatorContainerClassNames - = `connection-indicator indicator ${colorClass}`; - return ( -
-
-
- { this._renderIcon() } -
-
+
+ {this._renderIcon()}
); } @@ -369,4 +391,5 @@ export function _mapStateToProps(state: Object, ownProps: Props) { _connectionStatus: participant?.connectionStatus }; } -export default translate(connect(_mapStateToProps)(ConnectionIndicator)); +export default translate(connect(_mapStateToProps)( + withStyles(styles)(ConnectionIndicator))); diff --git a/react/features/connection-stats/components/ConnectionStatsTable.js b/react/features/connection-stats/components/ConnectionStatsTable.js index 110c2e3fd..7a26006e6 100644 --- a/react/features/connection-stats/components/ConnectionStatsTable.js +++ b/react/features/connection-stats/components/ConnectionStatsTable.js @@ -1,8 +1,10 @@ /* @flow */ +import { withStyles } from '@material-ui/styles'; import React, { Component } from 'react'; import { isMobileBrowser } from '../../../features/base/environment/utils'; +import ContextMenu from '../../base/components/context-menu/ContextMenu'; import { translate } from '../../base/i18n'; /** @@ -40,6 +42,11 @@ type Props = { */ bridgeCount: number, + /** + * An object containing the CSS classes. + */ + classes: Object, + /** * Audio/video codecs in use for the connection. */ @@ -163,6 +170,20 @@ function onClick(event) { event.stopPropagation(); } +const styles = theme => { + return { + contextMenu: { + position: 'relative', + marginTop: 0, + right: 'auto', + padding: `${theme.spacing(2)}px ${theme.spacing(1)}px`, + marginLeft: '4px', + marginRight: '4px', + marginBottom: '4px' + } + }; +}; + /** * React {@code Component} for displaying connection statistics. * @@ -176,20 +197,25 @@ class ConnectionStatsTable extends Component { * @returns {ReactElement} */ render() { - const { isLocalVideo, enableSaveLogs, disableShowMoreStats } = this.props; + const { isLocalVideo, enableSaveLogs, disableShowMoreStats, classes } = this.props; const className = isMobileBrowser() ? 'connection-info connection-info__mobile' : 'connection-info'; return ( -
- { this._renderStatistics() } -
- { isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null} - { !disableShowMoreStats && this._renderShowMoreLink() } +
+ ); } @@ -839,4 +865,4 @@ function getStringFromArray(array) { return res; } -export default translate(ConnectionStatsTable); +export default translate(withStyles(styles)(ConnectionStatsTable)); diff --git a/react/features/display-name/components/web/DisplayName.js b/react/features/display-name/components/web/DisplayName.js index b71f12f1a..ec2a03913 100644 --- a/react/features/display-name/components/web/DisplayName.js +++ b/react/features/display-name/components/web/DisplayName.js @@ -1,5 +1,6 @@ /* @flow */ +import { withStyles } from '@material-ui/styles'; import React, { Component } from 'react'; import type { Dispatch } from 'redux'; @@ -10,6 +11,8 @@ import { } from '../../../base/participants'; import { connect } from '../../../base/redux'; import { updateSettings } from '../../../base/settings'; +import { Tooltip } from '../../../base/tooltip'; +import { getIndicatorsTooltipPosition } from '../../../filmstrip/functions.web'; import { appendSuffix } from '../../functions'; /** @@ -33,6 +36,11 @@ type Props = { */ allowEditing: boolean, + /** + * The current layout of the filmstrip. + */ + currentLayout: string, + /** * Invoked to update the participant's display name. */ @@ -43,6 +51,11 @@ type Props = { */ displayNameSuffix: string, + /** + * An object containing the CSS classes. + */ + classes: Object, + /** * The ID attribute to add to the component. Useful for global querying for * the component by legacy components and torture tests. @@ -76,6 +89,30 @@ type State = { isEditing: boolean }; +const styles = theme => { + return { + displayName: { + ...theme.typography.labelBold, + lineHeight: `${theme.typography.labelBold.lineHeight}px`, + color: theme.palette.text01, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + }, + + editDisplayName: { + outline: 'none', + border: 'none', + background: 'none', + boxShadow: 'none', + padding: 0, + ...theme.typography.labelBold, + lineHeight: `${theme.typography.labelBold.lineHeight}px`, + color: theme.palette.text01 + } + }; +}; + /** * React {@code Component} for displaying and editing a participant's name. * @@ -146,7 +183,9 @@ class DisplayName extends Component { const { _nameToDisplay, allowEditing, + currentLayout, displayNameSuffix, + classes, elementID, t } = this.props; @@ -155,10 +194,11 @@ class DisplayName extends Component { return ( { } return ( - - { appendSuffix(_nameToDisplay, displayNameSuffix) } - + + + { appendSuffix(_nameToDisplay, displayNameSuffix) } + + ); } + /** + * Stop click event propagation. + * + * @param {MouseEvent} e - The click event. + * @private + * @returns {void} + */ + _onClick(e) { + e.stopPropagation(); + } + _onChange: () => void; /** @@ -215,11 +270,13 @@ class DisplayName extends Component { * Updates the component to display an editable input field and sets the * initial value to the current display name. * + * @param {MouseEvent} e - The click event. * @private * @returns {void} */ - _onStartEditing() { + _onStartEditing(e) { if (this.props.allowEditing) { + e.stopPropagation(); this.setState({ isEditing: true, editDisplayNameValue: this.props._configuredDisplayName @@ -292,4 +349,4 @@ function _mapStateToProps(state, ownProps) { }; } -export default translate(connect(_mapStateToProps)(DisplayName)); +export default translate(connect(_mapStateToProps)(withStyles(styles)(DisplayName))); diff --git a/react/features/filmstrip/components/web/AudioMutedIndicator.js b/react/features/filmstrip/components/web/AudioMutedIndicator.js index e6fdb8af7..fdf17da32 100644 --- a/react/features/filmstrip/components/web/AudioMutedIndicator.js +++ b/react/features/filmstrip/components/web/AudioMutedIndicator.js @@ -1,8 +1,8 @@ /* @flow */ -import React, { Component } from 'react'; +import React from 'react'; -import { IconMicDisabled } from '../../../base/icons'; +import { IconMicrophoneEmptySlash } from '../../../base/icons'; import { BaseIndicator } from '../../../base/react'; /** @@ -19,26 +19,16 @@ type Props = { /** * React {@code Component} for showing an audio muted icon with a tooltip. * - * @augments Component + * @returns {Component} */ -class AudioMutedIndicator extends Component { - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - return ( - - ); - } -} +const AudioMutedIndicator = ({ tooltipPosition }: Props) => ( + +); export default AudioMutedIndicator; diff --git a/react/features/filmstrip/components/web/DominantSpeakerIndicator.js b/react/features/filmstrip/components/web/DominantSpeakerIndicator.js deleted file mode 100644 index bd576685f..000000000 --- a/react/features/filmstrip/components/web/DominantSpeakerIndicator.js +++ /dev/null @@ -1,51 +0,0 @@ -/* @flow */ - -import React, { Component } from 'react'; - -import { IconDominantSpeaker } from '../../../base/icons'; -import { BaseIndicator } from '../../../base/react'; - -/** - * The type of the React {@code Component} props of - * {@link DominantSpeakerIndicator}. - */ -type Props = { - - /** - * The font-size for the icon. - */ - iconSize: number, - - /** - * From which side of the indicator the tooltip should appear from. - */ - tooltipPosition: string -}; - -/** - * Thumbnail badge showing that the participant is the dominant speaker in - * the conference. - * - * @augments Component - */ -class DominantSpeakerIndicator extends Component { - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - */ - render() { - return ( - - ); - } -} - -export default DominantSpeakerIndicator; diff --git a/react/features/filmstrip/components/web/ModeratorIndicator.js b/react/features/filmstrip/components/web/ModeratorIndicator.js index aa802d4ec..33c6817af 100644 --- a/react/features/filmstrip/components/web/ModeratorIndicator.js +++ b/react/features/filmstrip/components/web/ModeratorIndicator.js @@ -1,8 +1,8 @@ /* @flow */ -import React, { Component } from 'react'; +import React from 'react'; -import { IconModerator } from '../../../base/icons'; +import { IconCrown } from '../../../base/icons'; import { BaseIndicator } from '../../../base/react'; /** @@ -19,27 +19,14 @@ type Props = { /** * React {@code Component} for showing a moderator icon with a tooltip. * - * @augments Component + * @returns {Component} */ -class ModeratorIndicator extends Component { - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - return ( -
- -
- ); - } -} +const ModeratorIndicator = ({ tooltipPosition }: Props) => ( + +); export default ModeratorIndicator; diff --git a/react/features/filmstrip/components/web/RaisedHandIndicator.js b/react/features/filmstrip/components/web/RaisedHandIndicator.js index 848f81b7f..04dbaa267 100644 --- a/react/features/filmstrip/components/web/RaisedHandIndicator.js +++ b/react/features/filmstrip/components/web/RaisedHandIndicator.js @@ -1,53 +1,75 @@ /* @flow */ +import { makeStyles } from '@material-ui/styles'; import React from 'react'; +import { useSelector } from 'react-redux'; import { IconRaisedHand } from '../../../base/icons'; +import { getParticipantById, hasRaisedHand } from '../../../base/participants'; import { BaseIndicator } from '../../../base/react'; -import { connect } from '../../../base/redux'; -import AbstractRaisedHandIndicator, { - type Props as AbstractProps, - _mapStateToProps -} from '../AbstractRaisedHandIndicator'; /** * The type of the React {@code Component} props of {@link RaisedHandIndicator}. */ -type Props = AbstractProps & { +type Props = { /** * The font-size for the icon. */ iconSize: number, + /** + * The participant id who we want to render the raised hand indicator + * for. + */ + participantId: string, + /** * From which side of the indicator the tooltip should appear from. */ tooltipPosition: string }; +const useStyles = makeStyles(theme => { + return { + raisedHandIndicator: { + backgroundColor: theme.palette.warning01, + padding: '2px', + zIndex: 3, + display: 'inline-block', + borderRadius: '4px', + boxSizing: 'border-box' + } + }; +}); + /** * Thumbnail badge showing that the participant would like to speak. * - * @augments Component + * @returns {ReactElement} */ -class RaisedHandIndicator extends AbstractRaisedHandIndicator { - /** - * Renders the platform specific indicator element. - * - * @returns {React$Element<*>} - */ - _renderIndicator() { - return ( - - ); - } -} +const RaisedHandIndicator = ({ + iconSize, + participantId, + tooltipPosition +}: Props) => { + const _raisedHand = hasRaisedHand(useSelector(state => + getParticipantById(state, participantId))); + const styles = useStyles(); -export default connect(_mapStateToProps)(RaisedHandIndicator); + if (!_raisedHand) { + return null; + } + + return ( +
+ +
+ ); +}; + +export default RaisedHandIndicator; diff --git a/react/features/filmstrip/components/web/ScreenShareIndicator.js b/react/features/filmstrip/components/web/ScreenShareIndicator.js index e22538dc6..c7515a0e1 100644 --- a/react/features/filmstrip/components/web/ScreenShareIndicator.js +++ b/react/features/filmstrip/components/web/ScreenShareIndicator.js @@ -23,10 +23,9 @@ type Props = { export default function ScreenShareIndicator(props: Props) { return ( ); diff --git a/react/features/filmstrip/components/web/StatusIndicators.js b/react/features/filmstrip/components/web/StatusIndicators.js index 387725f3c..7fd790097 100644 --- a/react/features/filmstrip/components/web/StatusIndicators.js +++ b/react/features/filmstrip/components/web/StatusIndicators.js @@ -6,12 +6,12 @@ import { MEDIA_TYPE } from '../../../base/media'; import { getParticipantByIdOrUndefined, PARTICIPANT_ROLE } from '../../../base/participants'; import { connect } from '../../../base/redux'; import { getTrackByMediaTypeAndParticipant, isLocalTrackMuted, isRemoteTrackMuted } from '../../../base/tracks'; -import { getCurrentLayout, LAYOUTS } from '../../../video-layout'; +import { getCurrentLayout } from '../../../video-layout'; +import { getIndicatorsTooltipPosition } from '../../functions.web'; import AudioMutedIndicator from './AudioMutedIndicator'; import ModeratorIndicator from './ModeratorIndicator'; import ScreenShareIndicator from './ScreenShareIndicator'; -import VideoMutedIndicator from './VideoMutedIndicator'; declare var interfaceConfig: Object; @@ -40,11 +40,6 @@ type Props = { */ _showScreenShareIndicator: Boolean, - /** - * Indicates if the video muted indicator should be visible or not. - */ - _showVideoMutedIndicator: Boolean, - /** * The ID of the participant for which the status bar is rendered. */ @@ -68,29 +63,16 @@ class StatusIndicators extends Component { _currentLayout, _showAudioMutedIndicator, _showModeratorIndicator, - _showScreenShareIndicator, - _showVideoMutedIndicator + _showScreenShareIndicator } = this.props; - let tooltipPosition; - - switch (_currentLayout) { - case LAYOUTS.TILE_VIEW: - tooltipPosition = 'right'; - break; - case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: - tooltipPosition = 'left'; - break; - default: - tooltipPosition = 'top'; - } + const tooltipPosition = getIndicatorsTooltipPosition(_currentLayout); return ( -
- { _showAudioMutedIndicator ? : null } - { _showScreenShareIndicator ? : null } - { _showVideoMutedIndicator ? : null } - { _showModeratorIndicator ? : null } -
+ <> + { _showAudioMutedIndicator && } + { _showModeratorIndicator && } + { _showScreenShareIndicator && } + ); } } @@ -108,24 +90,21 @@ class StatusIndicators extends Component { * }} */ function _mapStateToProps(state, ownProps) { - const { participantID } = ownProps; + const { participantID, audio, moderator, screenshare } = ownProps; // Only the local participant won't have id for the time when the conference is not yet joined. const participant = getParticipantByIdOrUndefined(state, participantID); const tracks = state['features/base/tracks']; - let isVideoMuted = true; let isAudioMuted = true; let isScreenSharing = false; if (participant?.local) { - isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO); isAudioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO); } else if (!participant?.isFakeParticipant) { // remote participants excluding shared video const track = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID); isScreenSharing = track?.videoType === 'desktop'; - isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, participantID); isAudioMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID); } @@ -133,11 +112,10 @@ function _mapStateToProps(state, ownProps) { return { _currentLayout: getCurrentLayout(state), - _showAudioMutedIndicator: isAudioMuted, + _showAudioMutedIndicator: isAudioMuted && audio, _showModeratorIndicator: - !disableModeratorIndicator && participant && participant.role === PARTICIPANT_ROLE.MODERATOR, - _showScreenShareIndicator: isScreenSharing, - _showVideoMutedIndicator: isVideoMuted + !disableModeratorIndicator && participant && participant.role === PARTICIPANT_ROLE.MODERATOR && moderator, + _showScreenShareIndicator: isScreenSharing && screenshare }; } diff --git a/react/features/filmstrip/components/web/Thumbnail.js b/react/features/filmstrip/components/web/Thumbnail.js index b266401df..3a6aa0e4d 100644 --- a/react/features/filmstrip/components/web/Thumbnail.js +++ b/react/features/filmstrip/components/web/Thumbnail.js @@ -1,17 +1,15 @@ // @flow +import { withStyles } from '@material-ui/styles'; +import clsx from 'clsx'; import React, { Component } from 'react'; import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics'; -import { AudioLevelIndicator } from '../../../audio-level-indicator'; import { Avatar } from '../../../base/avatar'; -import { isNameReadOnly } from '../../../base/config'; import { isMobileBrowser } from '../../../base/environment/utils'; -import JitsiMeetJS from '../../../base/lib-jitsi-meet/_'; import { MEDIA_TYPE, VideoTrack } from '../../../base/media'; import { getParticipantByIdOrUndefined, - getParticipantCount, pinParticipant } from '../../../base/participants'; import { connect } from '../../../base/redux'; @@ -23,23 +21,19 @@ import { getTrackByMediaTypeAndParticipant, updateLastTrackVideoMediaEvent } from '../../../base/tracks'; -import { ConnectionIndicator } from '../../../connection-indicator'; -import { DisplayName } from '../../../display-name'; -import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip'; import { PresenceLabel } from '../../../presence-status'; import { getCurrentLayout, LAYOUTS } from '../../../video-layout'; -import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu'; -import { setVolume } from '../../actions.web'; import { DISPLAY_MODE_TO_CLASS_NAME, DISPLAY_VIDEO, - DISPLAY_VIDEO_WITH_NAME, VIDEO_TEST_EVENTS, SHOW_TOOLBAR_CONTEXT_MENU_AFTER } from '../../constants'; -import { isVideoPlayable, computeDisplayMode } from '../../functions'; +import { isVideoPlayable, computeDisplayModeFromInput, getDisplayModeInput } from '../../functions'; -const JitsiTrackEvents = JitsiMeetJS.events.track; +import ThumbnailAudioIndicator from './ThumbnailAudioIndicator'; +import ThumbnailBottomIndicators from './ThumbnailBottomIndicators'; +import ThumbnailTopIndicators from './ThumbnailTopIndicators'; declare var interfaceConfig: Object; @@ -48,11 +42,6 @@ declare var interfaceConfig: Object; */ export type State = {| - /** - * The current audio level value for the Thumbnail. - */ - audioLevel: number, - /** * Indicates that the canplay event has been received. */ @@ -63,15 +52,15 @@ export type State = {| */ displayMode: number, - /** - * Indicates whether the thumbnail is hovered or not. - */ - isHovered: boolean, - /** * Whether popover is visible or not. */ - popoverVisible: boolean + popoverVisible: boolean, + + /** + * Indicates whether the thumbnail is hovered or not. + */ + isHovered: boolean |}; /** @@ -79,36 +68,16 @@ export type State = {| */ export type Props = {| - /** - * If the display name is editable or not. - */ - _allowEditing: boolean, - /** * The audio track related to the participant. */ _audioTrack: ?Object, - /** - * Disable/enable the auto hide functionality for the connection indicator. - */ - _connectionIndicatorAutoHideEnabled: boolean, - - /** - * Disable/enable the connection indicator. - */ - _connectionIndicatorDisabled: boolean, - /** * The current layout of the filmstrip. */ _currentLayout: string, - /** - * The default display name for the local participant. - */ - _defaultLocalDisplayName: string, - /** * Indicates whether the local video flip feature is disabled or not. */ @@ -119,26 +88,21 @@ export type Props = {| */ _disableTileEnlargement: boolean, - /** - * The display mode of the thumbnail. - */ - _displayMode: number, - /** * The height of the Thumbnail. */ _height: number, - /** - * The aspect ratio of the Thumbnail in percents. - */ - _heightToWidthPercent: number, - /** * Indicates whether the thumbnail should be hidden or not. */ _isHidden: boolean, + /** + * Whether or not there is a pinned participant. + */ + _isAnyParticipantPinned: boolean, + /** * Indicates whether audio only mode is enabled. */ @@ -179,11 +143,6 @@ export type Props = {| */ _isTestModeEnabled: boolean, - /** - * The size of the icon of indicators. - */ - _indicatorIconSize: number, - /** * The current local video flip setting. */ @@ -195,25 +154,10 @@ export type Props = {| _participant: Object, /** - * True if there are more than 2 participants in the call. - */ - _participantCountMoreThan2: boolean, - - /** - * Indicates whether the "start silent" mode is enabled. - */ - _startSilent: Boolean, - - /** * The video track that will be displayed in the thumbnail. */ _videoTrack: ?Object, - /** - * The volume level for the thumbnail. - */ - _volume?: ?number, - /** * The width of the thumbnail. */ @@ -224,6 +168,11 @@ export type Props = {| */ dispatch: Function, + /** + * An object containing the CSS classes. + */ + classes: Object, + /** * The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view. */ @@ -240,17 +189,74 @@ export type Props = {| style?: ?Object |}; -/** - * Click handler for the display name container. - * - * @param {SyntheticEvent} event - The click event. - * @returns {void} - */ -function onClick(event) { - // If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation - // needs to be stopped. - event.stopPropagation(); -} +const defaultStyles = theme => { + return { + indicatorsContainer: { + position: 'absolute', + padding: `${theme.spacing(1)}px`, + zIndex: 10, + width: '100%', + boxSizing: 'border-box', + display: 'flex', + left: 0, + + '&.tile-view-mode': { + padding: `${theme.spacing(2)}px` + } + }, + + indicatorsTopContainer: { + top: 0, + justifyContent: 'space-between' + }, + + indicatorsBottomContainer: { + bottom: 0 + }, + + indicatorsBackground: { + backgroundColor: 'rgba(0, 0, 0, 0.7)', + borderRadius: '4px', + display: 'flex', + alignItems: 'center', + maxWidth: '100%', + overflow: 'hidden', + + '&:not(:empty)': { + padding: '2px' + }, + + '& > *:not(:last-child)': { + marginRight: '4px' + }, + + '&:not(.top-indicators) > *:last-child': { + marginRight: '6px' + } + }, + + containerBackground: { + position: 'absolute', + top: 0, + left: 0, + height: '100%', + width: '100%', + borderRadius: '4px', + backgroundColor: theme.palette.ui02 + }, + + activeSpeaker: { + '& .active-speaker-indicator': { + boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`, + position: 'absolute', + width: '100%', + height: '100%', + zIndex: '9', + borderRadius: '4px' + } + } + }; +}; /** * Implements a thumbnail. @@ -263,11 +269,6 @@ class Thumbnail extends Component { */ timeoutHandle: Object; - /** - * Reference to local or remote Video Menu trigger button instance. - */ - videoMenuTriggerRef: Object; - /** * Timeout used to detect double tapping. * It is active while user has tapped once. @@ -284,26 +285,21 @@ class Thumbnail extends Component { super(props); const state = { - audioLevel: 0, canPlayEventReceived: false, - isHovered: false, displayMode: DISPLAY_VIDEO, - popoverVisible: false + popoverVisible: false, + isHovered: false }; this.state = { ...state, - displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, state)), - popoverVisible: false + displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, state)) }; this.timeoutHandle = null; - this.videoMenuTriggerRef = null; this._clearDoubleClickTimeout = this._clearDoubleClickTimeout.bind(this); - this._updateAudioLevel = this._updateAudioLevel.bind(this); this._onCanPlay = this._onCanPlay.bind(this); this._onClick = this._onClick.bind(this); - this._onVolumeChange = this._onVolumeChange.bind(this); this._onMouseEnter = this._onMouseEnter.bind(this); this._onMouseLeave = this._onMouseLeave.bind(this); this._onTestingEvent = this._onTestingEvent.bind(this); @@ -321,7 +317,6 @@ class Thumbnail extends Component { * @returns {void} */ componentDidMount() { - this._listenForAudioUpdates(); this._onDisplayModeChanged(); } @@ -333,12 +328,6 @@ class Thumbnail extends Component { * @returns {void} */ componentDidUpdate(prevProps: Props, prevState: State) { - if (prevProps._audioTrack !== this.props._audioTrack) { - this._stopListeningForAudioUpdates(prevProps._audioTrack); - this._listenForAudioUpdates(); - this._updateAudioLevel(0); - } - if (prevState.displayMode !== this.state.displayMode) { this._onDisplayModeChanged(); } @@ -350,7 +339,7 @@ class Thumbnail extends Component { * @returns {void} */ _onDisplayModeChanged() { - const input = Thumbnail.getDisplayModeInput(this.props, this.state); + const input = getDisplayModeInput(this.props, this.state); this._maybeSendScreenSharingIssueEvents(input); } @@ -370,7 +359,7 @@ class Thumbnail extends Component { const { displayMode } = this.state; const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW; - if (![ DISPLAY_VIDEO, DISPLAY_VIDEO_WITH_NAME ].includes(displayMode) + if (!(DISPLAY_VIDEO === displayMode) && tileViewActive && _isScreenSharing && !_isAudioOnly) { @@ -395,11 +384,11 @@ class Thumbnail extends Component { return { ...newState, - displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, newState)) + displayMode: computeDisplayModeFromInput(getDisplayModeInput(props, newState)) }; } - const newDisplayMode = computeDisplayMode(Thumbnail.getDisplayModeInput(props, prevState)); + const newDisplayMode = computeDisplayModeFromInput(getDisplayModeInput(props, prevState)); if (newDisplayMode !== prevState.displayMode) { return { @@ -411,51 +400,6 @@ class Thumbnail extends Component { return null; } - /** - * Extracts information for props and state needed to compute the display mode. - * - * @param {Props} props - The component's props. - * @param {State} state - The component's state. - * @returns {Object} - */ - static getDisplayModeInput(props: Props, state: State) { - const { - _currentLayout, - _isAudioOnly, - _isCurrentlyOnLargeVideo, - _isScreenSharing, - _isVideoPlayable, - _participant, - _videoTrack - } = props; - const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW; - const { canPlayEventReceived, isHovered } = state; - - return { - isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo, - isHovered, - isAudioOnly: _isAudioOnly, - tileViewActive, - isVideoPlayable: _isVideoPlayable, - connectionStatus: _participant?.connectionStatus, - canPlayEventReceived, - videoStream: Boolean(_videoTrack), - isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local, - isScreenSharing: _isScreenSharing, - videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream' - }; - } - - /** - * Unsubscribe from audio level updates. - * - * @inheritdoc - * @returns {void} - */ - componentWillUnmount() { - this._stopListeningForAudioUpdates(this.props._audioTrack); - } - _clearDoubleClickTimeout: () => void; /** @@ -468,53 +412,6 @@ class Thumbnail extends Component { this._firstTap = undefined; } - /** - * Starts listening for audio level updates from the library. - * - * @private - * @returns {void} - */ - _listenForAudioUpdates() { - const { _audioTrack } = this.props; - - if (_audioTrack) { - const { jitsiTrack } = _audioTrack; - - jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel); - } - } - - /** - * Stops listening to further updates from the passed track. - * - * @param {Object} audioTrack - The track. - * @private - * @returns {void} - */ - _stopListeningForAudioUpdates(audioTrack) { - if (audioTrack) { - const { jitsiTrack } = audioTrack; - - jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel); - } - } - - _updateAudioLevel: (number) => void; - - /** - * Updates the internal state of the last know audio level. The level should - * be between 0 and 1, as the level will be used as a percentage out of 1. - * - * @param {number} audioLevel - The new audio level for the track. - * @private - * @returns {void} - */ - _updateAudioLevel(audioLevel) { - this.setState({ - audioLevel - }); - } - _showPopover: () => void; /** @@ -549,7 +446,6 @@ class Thumbnail extends Component { * @returns {Object} - The styles for the thumbnail. */ _getStyles(): Object { - const { canPlayEventReceived } = this.state; const { _currentLayout, @@ -575,7 +471,7 @@ class Thumbnail extends Component { video: {} }; - const avatarSize = _height / 2; + const avatarSize = Math.min(_height / 2, _width - 30); let { left } = style || {}; if (typeof left === 'number' && horizontalOffset) { @@ -730,67 +626,6 @@ class Thumbnail extends Component { ); } - /** - * Renders the top indicators of the thumbnail. - * - * @returns {Component} - */ - _renderTopIndicators() { - const { - _connectionIndicatorAutoHideEnabled, - _connectionIndicatorDisabled, - _currentLayout, - _isDominantSpeakerDisabled, - _indicatorIconSize: iconSize, - _participant, - _participantCountMoreThan2 - } = this.props; - const { isHovered } = this.state; - const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled; - const { id, dominantSpeaker = false } = _participant; - const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker; - let statsPopoverPosition, tooltipPosition; - - switch (_currentLayout) { - case LAYOUTS.TILE_VIEW: - statsPopoverPosition = 'right-start'; - tooltipPosition = 'right'; - break; - case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: - statsPopoverPosition = 'left-start'; - tooltipPosition = 'left'; - break; - case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: - statsPopoverPosition = 'top'; - tooltipPosition = 'top'; - break; - default: - statsPopoverPosition = 'auto'; - tooltipPosition = 'top'; - } - - return ( -
- { !_connectionIndicatorDisabled - && - } - - { showDominantSpeaker && _participantCountMoreThan2 - && - } -
); - } - /** * Renders the avatar. * @@ -820,115 +655,31 @@ class Thumbnail extends Component { _getContainerClassName() { let className = 'videocontainer'; const { displayMode } = this.state; - const { _isAudioOnly, _isDominantSpeakerDisabled, _isHidden, _participant } = this.props; - const isRemoteParticipant = !_participant?.local && !_participant?.isFakeParticipant; + const { + _isDominantSpeakerDisabled, + _participant, + _currentLayout, + _isAnyParticipantPinned, + classes + } = this.props; className += ` ${DISPLAY_MODE_TO_CLASS_NAME[displayMode]}`; - if (_participant?.pinned) { - className += ' videoContainerFocused'; - } - - if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) { - className += ' active-speaker'; - } - - if (_isHidden) { - className += ' hidden'; - } - - if (isRemoteParticipant && _isAudioOnly) { - className += ' audio-only'; + if (_currentLayout === LAYOUTS.TILE_VIEW) { + if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) { + className += ` ${classes.activeSpeaker} dominant-speaker`; + } + } else if (_isAnyParticipantPinned) { + if (_participant?.pinned) { + className += ` videoContainerFocused ${classes.activeSpeaker}`; + } + } else if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) { + className += ` ${classes.activeSpeaker} dominant-speaker`; } return className; } - /** - * Renders the local participant's thumbnail. - * - * @returns {ReactElement} - */ - _renderLocalParticipant() { - const { - _allowEditing, - _defaultLocalDisplayName, - _disableLocalVideoFlip, - _isMobile, - _isMobilePortrait, - _isScreenSharing, - _localFlipX, - _participant, - _videoTrack - } = this.props; - const { id } = _participant || {}; - const { audioLevel } = this.state; - const styles = this._getStyles(); - let containerClassName = this._getContainerClassName(); - const videoTrackClassName - = !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : ''; - - if (_isMobilePortrait) { - styles.thumbnail.height = styles.thumbnail.width; - containerClassName = `${containerClassName} self-view-mobile-portrait`; - } - - return ( - -
- - - -
- -
- -
-
-
- { this._renderTopIndicators() } -
-
- { this._renderAvatar(styles.avatar) } - - - - - - - - - ); - } - _onCanPlay: Object => void; /** @@ -971,40 +722,59 @@ class Thumbnail extends Component { /** * Renders a remote participant's 'thumbnail. * + * @param {boolean} local - Whether or not it's the local participant. * @returns {ReactElement} */ - _renderRemoteParticipant() { + _renderParticipant(local = false) { const { + _audioTrack, + _currentLayout, + _disableLocalVideoFlip, _isMobile, + _isMobilePortrait, + _isScreenSharing, _isTestModeEnabled, + _localFlipX, _participant, - _startSilent, _videoTrack, - _volume = 1 + classes } = this.props; - const { id } = _participant; - const { audioLevel } = this.state; + const { id } = _participant || {}; + const { isHovered, popoverVisible } = this.state; const styles = this._getStyles(); - const containerClassName = this._getContainerClassName(); - - // hide volume when in silent mode - const onVolumeChange = _startSilent ? undefined : this._onVolumeChange; + let containerClassName = this._getContainerClassName(); + const videoTrackClassName + = !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : ''; const jitsiVideoTrack = _videoTrack?.jitsiTrack; const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId(); const videoEventListeners = {}; - if (_videoTrack && _isTestModeEnabled) { - VIDEO_TEST_EVENTS.forEach(attribute => { - videoEventListeners[attribute] = this._onTestingEvent; - }); + if (local) { + if (_isMobilePortrait) { + styles.thumbnail.height = styles.thumbnail.width; + containerClassName = `${containerClassName} self-view-mobile-portrait`; + } + } else { + if (_videoTrack && _isTestModeEnabled) { + VIDEO_TEST_EVENTS.forEach(attribute => { + videoEventListeners[attribute] = this._onTestingEvent; + }); + } + videoEventListeners.onCanPlay = this._onCanPlay; } - videoEventListeners.onCanPlay = this._onCanPlay; + const video = _videoTrack && ; return ( { } ) } style = { styles.thumbnail }> - { - _videoTrack && - } -
-
- { this._renderTopIndicators() } + {local + ? {video} + : video} +
+
+
-
- -
- + +
+ { this._renderAvatar(styles.avatar) } + { !local && ( +
+
-
-
- { this._renderAvatar(styles.avatar) } -
- -
- - - - - - + )} + +
); } - _onVolumeChange: number => void; - - /** - * Handles volume changes. - * - * @param {number} value - The new value for the volume. - * @returns {void} - */ - _onVolumeChange(value) { - const { _participant, dispatch } = this.props; - const { id } = _participant; - - dispatch(setVolume(id, value)); - } - /** * Implements React's {@link Component#render()}. * @@ -1092,14 +848,14 @@ class Thumbnail extends Component { const { isFakeParticipant, local } = _participant; if (local) { - return this._renderLocalParticipant(); + return this._renderParticipant(true); } if (isFakeParticipant) { return this._renderFakeParticipant(); } - return this._renderRemoteParticipant(); + return this._renderParticipant(); } } @@ -1118,7 +874,6 @@ function _mapStateToProps(state, ownProps): Object { const id = participant?.id; const isLocal = participant?.local ?? true; const tracks = state['features/base/tracks']; - const { participantsVolume } = state['features/filmstrip']; const _videoTrack = isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID); const _audioTrack = isLocal @@ -1127,14 +882,12 @@ function _mapStateToProps(state, ownProps): Object { let size = {}; let _isMobilePortrait = false; const { - startSilent, defaultLocalDisplayName, disableLocalVideoFlip, disableTileEnlargement, iAmRecorder, iAmSipGateway } = state['features/base/config']; - const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {}; const { localFlipX } = state['features/base/settings']; const _isMobile = isMobileBrowser(); @@ -1167,7 +920,6 @@ function _mapStateToProps(state, ownProps): Object { break; } case LAYOUTS.TILE_VIEW: { - const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize; size = { @@ -1179,12 +931,7 @@ function _mapStateToProps(state, ownProps): Object { } return { - _allowEditing: !isNameReadOnly(state), _audioTrack, - _connectionIndicatorAutoHideEnabled: - Boolean(state['features/base/config'].connectionIndicators?.autoHide ?? true), - _connectionIndicatorDisabled: _isMobile - || Boolean(state['features/base/config'].connectionIndicators?.disabled), _currentLayout, _defaultLocalDisplayName: defaultLocalDisplayName, _disableLocalVideoFlip: Boolean(disableLocalVideoFlip), @@ -1198,15 +945,11 @@ function _mapStateToProps(state, ownProps): Object { _isScreenSharing: _videoTrack?.videoType === 'desktop', _isTestModeEnabled: isTestModeEnabled(state), _isVideoPlayable: id && isVideoPlayable(state, id), - _indicatorIconSize: NORMAL, _localFlipX: Boolean(localFlipX), _participant: participant, - _participantCountMoreThan2: getParticipantCount(state) > 2, - _startSilent: Boolean(startSilent), _videoTrack, - _volume: isLocal ? undefined : id ? participantsVolume[id] : undefined, ...size }; } -export default connect(_mapStateToProps)(Thumbnail); +export default connect(_mapStateToProps)(withStyles(defaultStyles)(Thumbnail)); diff --git a/react/features/filmstrip/components/web/ThumbnailAudioIndicator.js b/react/features/filmstrip/components/web/ThumbnailAudioIndicator.js new file mode 100644 index 000000000..e7d4717ee --- /dev/null +++ b/react/features/filmstrip/components/web/ThumbnailAudioIndicator.js @@ -0,0 +1,47 @@ +// @flow + +import React, { useEffect, useState } from 'react'; + +import { AudioLevelIndicator } from '../../../audio-level-indicator'; +import JitsiMeetJS from '../../../base/lib-jitsi-meet/_'; + +const JitsiTrackEvents = JitsiMeetJS.events.track; + +type Props = { + + /** + * The audio track related to the participant. + */ + _audioTrack: ?Object +} + +const ThumbnailAudioIndicator = ({ + _audioTrack +}: Props) => { + const [ audioLevel, setAudioLevel ] = useState(0); + + useEffect(() => { + setAudioLevel(0); + if (_audioTrack) { + const { jitsiTrack } = _audioTrack; + + jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, setAudioLevel); + } + + return () => { + if (_audioTrack) { + const { jitsiTrack } = _audioTrack; + + jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, setAudioLevel); + } + }; + }, [ _audioTrack ]); + + return ( + + + + ); +}; + +export default ThumbnailAudioIndicator; diff --git a/react/features/filmstrip/components/web/ThumbnailBottomIndicators.js b/react/features/filmstrip/components/web/ThumbnailBottomIndicators.js new file mode 100644 index 000000000..4b4c97f14 --- /dev/null +++ b/react/features/filmstrip/components/web/ThumbnailBottomIndicators.js @@ -0,0 +1,84 @@ +// @flow + +import { makeStyles } from '@material-ui/styles'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { isNameReadOnly } from '../../../base/config/functions.any'; +import DisplayName from '../../../display-name/components/web/DisplayName'; +import { LAYOUTS } from '../../../video-layout'; + +import StatusIndicators from './StatusIndicators'; + +declare var interfaceConfig: Object; + +type Props = { + + /** + * The current layout of the filmstrip. + */ + currentLayout: string, + + /** + * Class name for indicators container. + */ + className: string, + + /** + * Whether or not the indicators are for the local participant. + */ + local: boolean, + + /** + * Id of the participant for which the component is displayed. + */ + participantId: string +} + +const useStyles = makeStyles(() => { + return { + nameContainer: { + display: 'flex', + overflow: 'hidden', + padding: '2px 0', + + '&>div': { + display: 'flex', + overflow: 'hidden' + }, + + '&:first-child': { + marginLeft: '6px' + } + } + }; +}); + +const ThumbnailBottomIndicators = ({ + className, + currentLayout, + local, + participantId +}: Props) => { + const styles = useStyles(); + const _allowEditing = !useSelector(isNameReadOnly); + const _defaultLocalDisplayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME; + + return (
+ + + + +
); +}; + +export default ThumbnailBottomIndicators; diff --git a/react/features/filmstrip/components/web/ThumbnailTopIndicators.js b/react/features/filmstrip/components/web/ThumbnailTopIndicators.js new file mode 100644 index 000000000..a79ae692e --- /dev/null +++ b/react/features/filmstrip/components/web/ThumbnailTopIndicators.js @@ -0,0 +1,132 @@ +// @flow + +import { makeStyles } from '@material-ui/styles'; +import clsx from 'clsx'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { isMobileBrowser } from '../../../base/environment/utils'; +import ConnectionIndicator from '../../../connection-indicator/components/web/ConnectionIndicator'; +import { LAYOUTS } from '../../../video-layout'; +import { STATS_POPOVER_POSITION } from '../../constants'; +import { getIndicatorsTooltipPosition } from '../../functions.web'; + +import RaisedHandIndicator from './RaisedHandIndicator'; +import StatusIndicators from './StatusIndicators'; +import VideoMenuTriggerButton from './VideoMenuTriggerButton'; + +declare var interfaceConfig: Object; + +type Props = { + + /** + * The current layout of the filmstrip. + */ + currentLayout: string, + + /** + * Hide popover callback. + */ + hidePopover: Function, + + /** + * Class name for the status indicators container. + */ + indicatorsClassName: string, + + /** + * Whether or not the thumbnail is hovered. + */ + isHovered: boolean, + + /** + * Whether or not the indicators are for the local participant. + */ + local: boolean, + + /** + * Id of the participant for which the component is displayed. + */ + participantId: string, + + /** + * Whether popover is visible or not. + */ + popoverVisible: boolean, + + /** + * Show popover callback. + */ + showPopover: Function +} + +const useStyles = makeStyles(() => { + return { + container: { + display: 'flex', + + '& > *:not(:last-child)': { + marginRight: '4px' + } + } + }; +}); + +const ThumbnailTopIndicators = ({ + currentLayout, + hidePopover, + indicatorsClassName, + isHovered, + local, + participantId, + popoverVisible, + showPopover +}: Props) => { + const styles = useStyles(); + + const _isMobile = isMobileBrowser(); + const { NORMAL = 16 } = interfaceConfig.INDICATOR_FONT_SIZES || {}; + const _indicatorIconSize = NORMAL; + const _connectionIndicatorAutoHideEnabled = Boolean( + useSelector(state => state['features/base/config'].connectionIndicators?.autoHide) ?? true); + const _connectionIndicatorDisabled = _isMobile + || Boolean(useSelector(state => state['features/base/config'].connectionIndicators?.disabled)); + + const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled; + + return ( + <> +
+ {!_connectionIndicatorDisabled + && + } + + {currentLayout === LAYOUTS.TILE_VIEW && ( +
+ +
+ )} +
+
+ +
+ ); +}; + +export default ThumbnailTopIndicators; diff --git a/react/features/filmstrip/components/web/ThumbnailWrapper.js b/react/features/filmstrip/components/web/ThumbnailWrapper.js index d9655bb2d..0a23ed8eb 100644 --- a/react/features/filmstrip/components/web/ThumbnailWrapper.js +++ b/react/features/filmstrip/components/web/ThumbnailWrapper.js @@ -23,6 +23,11 @@ type Props = { */ _horizontalOffset: number, + /** + * Whether or not there is a pinned participant. + */ + _isAnyParticipantPinned: boolean, + /** * The ID of the participant associated with the Thumbnail. */ @@ -75,7 +80,7 @@ class ThumbnailWrapper extends Component { * @returns {ReactElement} */ render() { - const { _participantID, style, _horizontalOffset = 0, _disableSelfView } = this.props; + const { _participantID, style, _horizontalOffset = 0, _isAnyParticipantPinned, _disableSelfView } = this.props; if (typeof _participantID !== 'string') { return null; @@ -91,6 +96,7 @@ class ThumbnailWrapper extends Component { return ( { function _mapStateToProps(state, ownProps) { const _currentLayout = getCurrentLayout(state); const { remoteParticipants } = state['features/filmstrip']; + const { remote, local } = state['features/base/participants']; const remoteParticipantsLength = remoteParticipants.length; const { testing = {} } = state['features/base/config']; const disableSelfView = getDisableSelfView(state); @@ -160,8 +167,11 @@ function _mapStateToProps(state, ownProps) { return {}; } + const _isAnyParticipantPinned = Boolean([ ...remote ].find(([ , value ]) => value?.pinned) || local?.pinned); + return { - _participantID: remoteParticipants[index] + _participantID: remoteParticipants[index], + _isAnyParticipantPinned }; } diff --git a/react/features/filmstrip/components/web/VideoMenuTriggerButton.js b/react/features/filmstrip/components/web/VideoMenuTriggerButton.js new file mode 100644 index 000000000..20d35af18 --- /dev/null +++ b/react/features/filmstrip/components/web/VideoMenuTriggerButton.js @@ -0,0 +1,69 @@ +// @flow + +import React from 'react'; + +import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu'; + +type Props = { + + /** + * Hide popover callback. + */ + hidePopover: Function, + + /** + * Whether or not the button is for the local participant. + */ + local: boolean, + + /** + * The id of the participant for which the button is. + */ + participantId?: string, + + /** + * Whether popover is visible or not. + */ + popoverVisible: boolean, + + /** + * Show popover callback. + */ + showPopover: Function, + + /** + * Whether or not the component is visible. + */ + visible: boolean +} + +// eslint-disable-next-line no-confusing-arrow +const VideoMenuTriggerButton = ({ + hidePopover, + local, + participantId, + popoverVisible, + showPopover, + visible +}: Props) => local + ? ( + + + + ) + : ( + + + + ); + +export default VideoMenuTriggerButton; diff --git a/react/features/filmstrip/components/web/VideoMutedIndicator.js b/react/features/filmstrip/components/web/VideoMutedIndicator.js deleted file mode 100644 index 88c104d78..000000000 --- a/react/features/filmstrip/components/web/VideoMutedIndicator.js +++ /dev/null @@ -1,43 +0,0 @@ -/* @flow */ - -import React, { Component } from 'react'; - -import { IconCameraDisabled } from '../../../base/icons'; -import { BaseIndicator } from '../../../base/react'; - -/** - * The type of the React {@code Component} props of {@link VideoMutedIndicator}. - */ -type Props = { - - /** - * From which side of the indicator the tooltip should appear from. - */ - tooltipPosition: string -}; - -/** - * React {@code Component} for showing a video muted icon with a tooltip. - * - * @augments Component - */ -class VideoMutedIndicator extends Component { - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - */ - render() { - return ( - - ); - } -} - -export default VideoMutedIndicator; diff --git a/react/features/filmstrip/components/web/index.js b/react/features/filmstrip/components/web/index.js index 410fdcf2b..cbb17a39d 100644 --- a/react/features/filmstrip/components/web/index.js +++ b/react/features/filmstrip/components/web/index.js @@ -1,10 +1,8 @@ // @flow export { default as AudioMutedIndicator } from './AudioMutedIndicator'; -export { default as DominantSpeakerIndicator } from './DominantSpeakerIndicator'; export { default as Filmstrip } from './Filmstrip'; export { default as ModeratorIndicator } from './ModeratorIndicator'; export { default as RaisedHandIndicator } from './RaisedHandIndicator'; export { default as StatusIndicators } from './StatusIndicators'; -export { default as VideoMutedIndicator } from './VideoMutedIndicator'; export { default as Thumbnail } from './Thumbnail'; diff --git a/react/features/filmstrip/constants.js b/react/features/filmstrip/constants.js index 4be05f119..32950ae6e 100644 --- a/react/features/filmstrip/constants.js +++ b/react/features/filmstrip/constants.js @@ -1,6 +1,7 @@ // @flow import { BoxModel } from '../base/styles'; +import { LAYOUTS } from '../video-layout/constants'; /** * The size (height and width) of the small (not tile view) thumbnails. @@ -96,33 +97,6 @@ export const DISPLAY_VIDEO = 0; */ export const DISPLAY_AVATAR = 1; -/** - * Display mode constant used when neither video nor avatar is being displayed - * on the small video. And we just show the display name. - * - * @type {number} - * @constant - */ -export const DISPLAY_BLACKNESS_WITH_NAME = 2; - -/** - * Display mode constant used when video is displayed and display name - * at the same time. - * - * @type {number} - * @constant - */ -export const DISPLAY_VIDEO_WITH_NAME = 3; - -/** - * Display mode constant used when neither video nor avatar is being displayed - * on the small video. And we just show the display name. - * - * @type {number} - * @constant - */ -export const DISPLAY_AVATAR_WITH_NAME = 4; - /** * Maps the display modes to class name that will be applied on the thumbnail container. * @@ -131,24 +105,7 @@ export const DISPLAY_AVATAR_WITH_NAME = 4; */ export const DISPLAY_MODE_TO_CLASS_NAME = [ 'display-video', - 'display-avatar-only', - 'display-name-on-black', - 'display-name-on-video', - 'display-avatar-with-name' -]; - -/** - * Maps the display modes to string. - * - * @type {Array} - * @constant - */ -export const DISPLAY_MODE_TO_STRING = [ - 'video', - 'avatar', - 'blackness-with-name', - 'video-with-name', - 'avatar-with-name' + 'display-avatar-only' ]; /** @@ -165,6 +122,20 @@ export const TILE_VERTICAL_MARGIN = 4; */ export const TILE_HORIZONTAL_MARGIN = 4; +/** + * The vertical margin of the tile grid container. + * + * @type {number} + */ +export const TILE_VIEW_GRID_VERTICAL_MARGIN = 12; + +/** + * The horizontal margin of the tile grid container. + * + * @type {number} + */ +export const TILE_VIEW_GRID_HORIZONTAL_MARGIN = 12; + /** * The height of the whole toolbar. */ @@ -238,3 +209,21 @@ export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600; * @type {number} */ export const TILE_MARGIN = 10; + +/** + * The popover position for the connection stats table. + */ +export const STATS_POPOVER_POSITION = { + [LAYOUTS.TILE_VIEW]: 'right-start', + [LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'left-start', + [LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'top-end' +}; + +/** + * The tooltip position for the indicators on the thumbnail. + */ +export const INDICATORS_TOOLTIP_POSITION = { + [LAYOUTS.TILE_VIEW]: 'right', + [LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'left', + [LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'top' +}; diff --git a/react/features/filmstrip/functions.web.js b/react/features/filmstrip/functions.web.js index d0c9ab757..1398ec7e7 100644 --- a/react/features/filmstrip/functions.web.js +++ b/react/features/filmstrip/functions.web.js @@ -15,20 +15,21 @@ import { isLocalTrackMuted, isRemoteTrackMuted } from '../base/tracks/functions'; +import { LAYOUTS } from '../video-layout'; import { ASPECT_RATIO_BREAKPOINT, DISPLAY_AVATAR, - DISPLAY_AVATAR_WITH_NAME, - DISPLAY_BLACKNESS_WITH_NAME, DISPLAY_VIDEO, - DISPLAY_VIDEO_WITH_NAME, + INDICATORS_TOOLTIP_POSITION, SCROLL_SIZE, SQUARE_TILE_ASPECT_RATIO, STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER, TILE_ASPECT_RATIO, TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN, + TILE_VIEW_GRID_HORIZONTAL_MARGIN, + TILE_VIEW_GRID_VERTICAL_MARGIN, VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN } from './constants'; @@ -190,8 +191,8 @@ export function calculateThumbnailSizeForTileView({ aspectRatio = SQUARE_TILE_ASPECT_RATIO; } - const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN); - const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN); + const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN) - TILE_VIEW_GRID_HORIZONTAL_MARGIN; + const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN; const initialWidth = viewWidth / columns; const initialHeight = viewHeight / minVisibleRows; const aspectRatioHeight = initialWidth / aspectRatio; @@ -240,32 +241,75 @@ export function getVerticalFilmstripVisibleAreaWidth() { /** * Computes information that determine the display mode. * - * @param {Object} input - Obejct containing all necessary information for determining the display mode for + * @param {Object} input - Object containing all necessary information for determining the display mode for * the thumbnail. - * @returns {number} - One of DISPLAY_VIDEO, DISPLAY_AVATAR or DISPLAY_BLACKNESS_WITH_NAME. + * @returns {number} - One of DISPLAY_VIDEO or DISPLAY_AVATAR. */ -export function computeDisplayMode(input: Object) { +export function computeDisplayModeFromInput(input: Object) { const { isAudioOnly, isCurrentlyOnLargeVideo, isScreenSharing, canPlayEventReceived, - isHovered, isRemoteParticipant, tileViewActive } = input; const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived); if (!tileViewActive && isScreenSharing && isRemoteParticipant) { - return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR; + return DISPLAY_AVATAR; } else if (isCurrentlyOnLargeVideo && !tileViewActive) { // Display name is always and only displayed when user is on the stage - return adjustedIsVideoPlayable && !isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME; + return adjustedIsVideoPlayable && !isAudioOnly ? DISPLAY_VIDEO : DISPLAY_AVATAR; } else if (adjustedIsVideoPlayable && !isAudioOnly) { // check hovering and change state to video with name - return isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO; + return DISPLAY_VIDEO; } // check hovering and change state to avatar with name - return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR; + return DISPLAY_AVATAR; +} + +/** + * Extracts information for props and state needed to compute the display mode. + * + * @param {Object} props - The Thumbnail component's props. + * @param {Object} state - The Thumbnail component's state. + * @returns {Object} +*/ +export function getDisplayModeInput(props: Object, state: Object) { + const { + _currentLayout, + _isAudioOnly, + _isCurrentlyOnLargeVideo, + _isScreenSharing, + _isVideoPlayable, + _participant, + _videoTrack + } = props; + const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW; + const { canPlayEventReceived } = state; + + return { + isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo, + isAudioOnly: _isAudioOnly, + tileViewActive, + isVideoPlayable: _isVideoPlayable, + connectionStatus: _participant?.connectionStatus, + canPlayEventReceived, + videoStream: Boolean(_videoTrack), + isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local, + isScreenSharing: _isScreenSharing, + videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream' + }; +} + +/** + * Gets the tooltip position for the thumbnail indicators. + * + * @param {string} currentLayout - The current layout of the app. + * @returns {string} + */ +export function getIndicatorsTooltipPosition(currentLayout: string) { + return INDICATORS_TOOLTIP_POSITION[currentLayout] || 'top'; } diff --git a/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js b/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js index 315f76376..a6f297107 100644 --- a/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js +++ b/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js @@ -1,87 +1,16 @@ // @flow -import { withStyles } from '@material-ui/styles'; import React, { Component } from 'react'; -import { createBreakoutRoomsEvent, sendAnalytics } from '../../../analytics'; -import { approveParticipant } from '../../../av-moderation/actions'; -import { Avatar } from '../../../base/avatar'; -import { ContextMenu, ContextMenuItemGroup } from '../../../base/components'; -import { isToolbarButtonEnabled } from '../../../base/config/functions.web'; -import { openDialog } from '../../../base/dialog'; -import { isIosMobileBrowser } from '../../../base/environment/utils'; import { translate } from '../../../base/i18n'; -import { - IconCloseCircle, - IconCrown, - IconMessage, - IconMicDisabled, - IconMicrophone, - IconMuteEveryoneElse, - IconRingGroup, - IconShareVideo, - IconVideoOff -} from '../../../base/icons'; -import { MEDIA_TYPE } from '../../../base/media'; import { getLocalParticipant, - getParticipantByIdOrUndefined, - isLocalParticipantModerator, - isParticipantModerator + getParticipantByIdOrUndefined } from '../../../base/participants'; import { connect } from '../../../base/redux'; -import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks'; -import { sendParticipantToRoom } from '../../../breakout-rooms/actions'; -import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions'; -import { openChatById } from '../../../chat/actions'; -import { setVolume } from '../../../filmstrip/actions.web'; -import { stopSharedVideo } from '../../../shared-video/actions.any'; -import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu'; -import { VolumeSlider } from '../../../video-menu/components/web'; -import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog'; -import { isForceMuted } from '../../functions'; +import ParticipantContextMenu from '../../../video-menu/components/web/ParticipantContextMenu'; type Props = { - /** - * Whether or not the participant is audio force muted. - */ - _isAudioForceMuted: boolean, - - /** - * The id of the current room. - */ - _currentRoomId: String, - - /** - * True if the local participant is moderator and false otherwise. - */ - _isLocalModerator: boolean, - - /** - * True if the chat button is enabled and false otherwise. - */ - _isChatButtonEnabled: boolean, - - /** - * True if the participant is moderator and false otherwise. - */ - _isParticipantModerator: boolean, - - /** - * True if the participant is video muted and false otherwise. - */ - _isParticipantVideoMuted: boolean, - - /** - * True if the participant is audio muted and false otherwise. - */ - _isParticipantAudioMuted: boolean, - - /** - * Whether or not the participant is video force muted. - */ - _isVideoForceMuted: boolean, - /** * Shared video local participant owner. */ @@ -92,27 +21,11 @@ type Props = { */ _participant: Object, - /** - * Rooms reference. - */ - _rooms: Array, - - /** - * A value between 0 and 1 indicating the volume of the participant's - * audio element. - */ - _volume: ?number, - /** * Closes a drawer if open. */ closeDrawer: Function, - /** - * An object containing the CSS classes. - */ - classes?: {[ key: string]: string}, - /** * The dispatch function from redux. */ @@ -124,11 +37,6 @@ type Props = { */ drawerParticipant: Object, - /** - * Callback used to open a confirmation dialog for audio muting. - */ - muteAudio: Function, - /** * Target elements against which positioning calculations are made. */ @@ -152,31 +60,7 @@ type Props = { /** * The ID of the participant. */ - participantID?: string, - - /** - * True if an overflow drawer should be displayed. - */ - overflowDrawer: boolean, - - /** - * The translate function. - */ - t: Function -}; - -const styles = theme => { - return { - text: { - color: theme.palette.text02, - padding: '10px 16px', - height: '40px', - overflow: 'hidden', - display: 'flex', - alignItems: 'center', - boxSizing: 'border-box' - } - }; + participantID: string }; /** @@ -184,166 +68,6 @@ const styles = theme => { */ class MeetingParticipantContextMenu extends Component { - /** - * Creates new instance of MeetingParticipantContextMenu. - * - * @param {Props} props - The props. - */ - constructor(props: Props) { - super(props); - - this._getCurrentParticipantId = this._getCurrentParticipantId.bind(this); - this._onGrantModerator = this._onGrantModerator.bind(this); - this._onKick = this._onKick.bind(this); - this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this); - this._onMuteVideo = this._onMuteVideo.bind(this); - this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this); - this._onStopSharedVideo = this._onStopSharedVideo.bind(this); - this._onSendToRoom = this._onSendToRoom.bind(this); - this._onVolumeChange = this._onVolumeChange.bind(this); - this._onAskToUnmute = this._onAskToUnmute.bind(this); - } - - _getCurrentParticipantId: () => string; - - /** - * Returns the participant id for the item we want to operate. - * - * @returns {void} - */ - _getCurrentParticipantId() { - const { _participant, drawerParticipant, overflowDrawer } = this.props; - - return overflowDrawer ? drawerParticipant?.participantID : _participant?.id; - } - - _onGrantModerator: () => void; - - /** - * Grant moderator permissions. - * - * @returns {void} - */ - _onGrantModerator() { - this.props.dispatch(openDialog(GrantModeratorDialog, { - participantID: this._getCurrentParticipantId() - })); - } - - _onKick: () => void; - - /** - * Kicks the participant. - * - * @returns {void} - */ - _onKick() { - this.props.dispatch(openDialog(KickRemoteParticipantDialog, { - participantID: this._getCurrentParticipantId() - })); - } - - _onStopSharedVideo: () => void; - - /** - * Stops shared video. - * - * @returns {void} - */ - _onStopSharedVideo() { - const { dispatch, onSelect } = this.props; - - onSelect(true); - dispatch(stopSharedVideo()); - } - - _onMuteEveryoneElse: () => void; - - /** - * Mutes everyone else. - * - * @returns {void} - */ - _onMuteEveryoneElse() { - this.props.dispatch(openDialog(MuteEveryoneDialog, { - exclude: [ this._getCurrentParticipantId() ] - })); - } - - _onMuteVideo: () => void; - - /** - * Mutes the video of the selected participant. - * - * @returns {void} - */ - _onMuteVideo() { - this.props.dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { - participantID: this._getCurrentParticipantId() - })); - } - - _onSendPrivateMessage: () => void; - - /** - * Sends private message. - * - * @returns {void} - */ - _onSendPrivateMessage() { - const { dispatch } = this.props; - - dispatch(openChatById(this._getCurrentParticipantId())); - } - - _onSendToRoom: (room: Object) => void; - - /** - * Sends a participant to a room. - * - * @param {Object} room - The room that the participant should be moved to. - * @returns {void} - */ - _onSendToRoom(room: Object) { - return () => { - const { _participant, dispatch, onSelect } = this.props; - - onSelect(true); - sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room')); - dispatch(sendParticipantToRoom(_participant.id, room.id)); - }; - } - - _onVolumeChange: (number) => void; - - /** - * Handles volume changes. - * - * @param {number} value - The new value for the volume. - * @returns {void} - */ - _onVolumeChange(value) { - const { _participant, dispatch } = this.props; - const { id } = _participant; - - dispatch(setVolume(id, value)); - } - - _onAskToUnmute: () => void; - - /** - * Handles click on ask to unmute. - * - * @returns {void} - */ - _onAskToUnmute() { - const { _participant, dispatch } = this.props; - const { id } = _participant; - - dispatch(approveParticipant(id)); - } - - /** * Implements React's {@link Component#render()}. * @@ -352,165 +76,31 @@ class MeetingParticipantContextMenu extends Component { */ render() { const { - _isAudioForceMuted, - _currentRoomId, - _isLocalModerator, - _isChatButtonEnabled, - _isParticipantModerator, - _isParticipantVideoMuted, - _isParticipantAudioMuted, - _isVideoForceMuted, _localVideoOwner, _participant, - _rooms, - _volume = 1, - classes, closeDrawer, drawerParticipant, offsetTarget, onEnter, onLeave, - onSelect, - overflowDrawer, - muteAudio, - t + onSelect } = this.props; if (!_participant) { return null; } - const showVolumeSlider = !isIosMobileBrowser() - && overflowDrawer - && typeof _volume === 'number' - && !isNaN(_volume); - - const fakeParticipantActions = [ { - accessibilityLabel: t('toolbar.stopSharedVideo'), - icon: IconShareVideo, - onClick: this._onStopSharedVideo, - text: t('toolbar.stopSharedVideo') - } ]; - - const moderatorActions1 = [ - overflowDrawer && (_isAudioForceMuted || _isVideoForceMuted) ? { - accessibilityLabel: t(_isAudioForceMuted - ? 'participantsPane.actions.askUnmute' - : 'participantsPane.actions.allowVideo'), - icon: IconMicrophone, - onClick: this._onAskToUnmute, - text: t(_isAudioForceMuted - ? 'participantsPane.actions.askUnmute' - : 'participantsPane.actions.allowVideo') - } : null, - !_isParticipantAudioMuted && overflowDrawer ? { - accessibilityLabel: t('dialog.muteParticipantButton'), - icon: IconMicDisabled, - onClick: muteAudio(_participant), - text: t('dialog.muteParticipantButton') - } : null, { - accessibilityLabel: t('toolbar.accessibilityLabel.muteEveryoneElse'), - icon: IconMuteEveryoneElse, - onClick: this._onMuteEveryoneElse, - text: t('toolbar.accessibilityLabel.muteEveryoneElse') - }, - _isParticipantVideoMuted ? null : { - accessibilityLabel: t('participantsPane.actions.stopVideo'), - icon: IconVideoOff, - onClick: this._onMuteVideo, - text: t('participantsPane.actions.stopVideo') - } - ].filter(Boolean); - - const moderatorActions2 = [ - _isLocalModerator && !_isParticipantModerator ? { - accessibilityLabel: t('toolbar.accessibilityLabel.grantModerator'), - icon: IconCrown, - onClick: this._onGrantModerator, - text: t('toolbar.accessibilityLabel.grantModerator') - } : null, - _isLocalModerator ? { - accessibilityLabel: t('videothumbnail.kick'), - icon: IconCloseCircle, - onClick: this._onKick, - text: t('videothumbnail.kick') - } : null, - _isChatButtonEnabled ? { - accessibilityLabel: t('toolbar.accessibilityLabel.privateMessage'), - icon: IconMessage, - onClick: this._onSendPrivateMessage, - text: t('toolbar.accessibilityLabel.privateMessage') - } : null - ].filter(Boolean); - - const breakoutRoomActions = _rooms.map(room => { - if (room.id !== _currentRoomId) { - return { - accessibilityLabel: room.name || t('breakoutRooms.mainRoom'), - icon: IconRingGroup, - onClick: this._onSendToRoom(room), - text: room.name || t('breakoutRooms.mainRoom') - }; - } - - return null; - } - ).filter(Boolean); - - const actions - = _participant?.isFakeParticipant ? ( - <> - {_localVideoOwner && ( - - )} - - ) : ( - <> - {_isLocalModerator - && - } - - - - { - _isLocalModerator && _rooms.length > 1 - && -
- {t('breakoutRooms.actions.sendToBreakoutRoom')} -
-
- } - { showVolumeSlider - && - - - } - - ); - return ( - - {overflowDrawer && , - text: drawerParticipant && drawerParticipant.displayName - } ] } />} - {actions} - + onEnter = { onEnter } + onLeave = { onLeave } + onSelect = { onSelect } + participant = { _participant } + thumbnailMenu = { false } /> ); } } @@ -531,32 +121,10 @@ function _mapStateToProps(state, ownProps): Object { const participant = getParticipantByIdOrUndefined(state, overflowDrawer ? drawerParticipant?.participantID : participantID); - const _currentRoomId = getCurrentRoomId(state); - const _isLocalModerator = isLocalParticipantModerator(state); - const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state); - const _isParticipantVideoMuted = isParticipantVideoMuted(participant, state); - const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state); - const _isParticipantModerator = isParticipantModerator(participant); - const _rooms = Object.values(getBreakoutRooms(state)); - - const { participantsVolume } = state['features/filmstrip']; - const id = participant?.id; - const isLocal = participant?.local ?? true; - return { - _isAudioForceMuted: isForceMuted(participant, MEDIA_TYPE.AUDIO, state), - _currentRoomId, - _isLocalModerator, - _isChatButtonEnabled, - _isParticipantModerator, - _isParticipantVideoMuted, - _isParticipantAudioMuted, - _isVideoForceMuted: isForceMuted(participant, MEDIA_TYPE.VIDEO, state), _localVideoOwner: Boolean(ownerId === localParticipantId), - _participant: participant, - _rooms, - _volume: isLocal ? undefined : id ? participantsVolume[id] : undefined + _participant: participant }; } -export default translate(connect(_mapStateToProps)(withStyles(styles)(MeetingParticipantContextMenu))); +export default translate(connect(_mapStateToProps)(MeetingParticipantContextMenu)); diff --git a/react/features/participants-pane/components/web/RaisedHandIndicator.js b/react/features/participants-pane/components/web/RaisedHandIndicator.js index 491b0d447..41afa0762 100644 --- a/react/features/participants-pane/components/web/RaisedHandIndicator.js +++ b/react/features/participants-pane/components/web/RaisedHandIndicator.js @@ -8,7 +8,7 @@ import { Icon, IconRaisedHandHollow } from '../../../base/icons'; const useStyles = makeStyles(theme => { return { indicator: { - backgroundColor: theme.palette.warning02, + backgroundColor: theme.palette.warning01, borderRadius: `${theme.shape.borderRadius / 2}px`, height: '24px', width: '24px' diff --git a/react/features/participants-pane/constants.js b/react/features/participants-pane/constants.js index d865e372c..f1210cb70 100644 --- a/react/features/participants-pane/constants.js +++ b/react/features/participants-pane/constants.js @@ -95,11 +95,13 @@ export const VideoStateIcons = { [MEDIA_STATE.FORCE_MUTED]: ( ), [MEDIA_STATE.MUTED]: ( ), diff --git a/react/features/toolbox/components/MuteEveryonesVideoButton.js b/react/features/toolbox/components/MuteEveryonesVideoButton.js index c0f79a99d..4248bf2c8 100644 --- a/react/features/toolbox/components/MuteEveryonesVideoButton.js +++ b/react/features/toolbox/components/MuteEveryonesVideoButton.js @@ -27,7 +27,7 @@ type Props = AbstractButtonProps & { * every participant (except the local one). */ class MuteEveryonesVideoButton extends AbstractButton { - accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideo'; + accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideoStream'; icon = IconMuteVideoEveryone; label = 'toolbar.muteEveryonesVideo'; tooltip = 'toolbar.muteEveryonesVideo'; diff --git a/react/features/video-menu/components/AbstractMuteButton.js b/react/features/video-menu/components/AbstractMuteButton.js index d7f8e81af..2074319b5 100644 --- a/react/features/video-menu/components/AbstractMuteButton.js +++ b/react/features/video-menu/components/AbstractMuteButton.js @@ -4,6 +4,7 @@ import { createRemoteVideoMenuButtonEvent, sendAnalytics } from '../../analytics'; +import { rejectParticipantAudio } from '../../av-moderation/actions'; import { IconMicDisabled } from '../../base/icons'; import { MEDIA_TYPE } from '../../base/media'; import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; @@ -54,12 +55,13 @@ export default class AbstractMuteButton extends AbstractButton { const { dispatch, participantID } = this.props; sendAnalytics(createRemoteVideoMenuButtonEvent( - 'mute.button', + 'mute', { 'participant_id': participantID })); dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO)); + dispatch(rejectParticipantAudio(participantID)); } /** diff --git a/react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js b/react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js index 750f3a6d7..0265cc289 100644 --- a/react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js +++ b/react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js @@ -29,7 +29,7 @@ export type Props = AbstractButtonProps & { * An abstract remote video menu button which disables the camera of all the other participants. */ export default class AbstractMuteEveryoneElsesVideoButton extends AbstractButton { - accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideo'; + accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideoStream'; icon = IconMuteVideoEveryone; label = 'videothumbnail.domuteVideoOfOthers'; diff --git a/react/features/video-menu/components/web/AskToUnmuteButton.js b/react/features/video-menu/components/web/AskToUnmuteButton.js new file mode 100644 index 000000000..b119b628e --- /dev/null +++ b/react/features/video-menu/components/web/AskToUnmuteButton.js @@ -0,0 +1,49 @@ +// @flow + +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; + +import { approveParticipant } from '../../../av-moderation/actions'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; +import { IconMicrophoneEmpty } from '../../../base/icons'; + +type Props = { + + /** + * Whether or not the participant is audio force muted. + */ + isAudioForceMuted: boolean, + + /** + * Whether or not the participant is video force muted. + */ + isVideoForceMuted: boolean, + + /** + * The ID for the participant on which the button will act. + */ + participantID: string +} + +const AskToUnmuteButton = ({ isAudioForceMuted, isVideoForceMuted, participantID }: Props) => { + const dispatch = useDispatch(); + const { t } = useTranslation(); + const _onClick = useCallback(() => { + dispatch(approveParticipant(participantID)); + }, [ participantID ]); + + const text = isAudioForceMuted || !isVideoForceMuted + ? t('participantsPane.actions.askUnmute') + : t('participantsPane.actions.allowVideo'); + + return ( + + ); +}; + +export default AskToUnmuteButton; diff --git a/react/features/video-menu/components/web/ConnectionStatusButton.js b/react/features/video-menu/components/web/ConnectionStatusButton.js index a60bcb00f..7f87fa58b 100644 --- a/react/features/video-menu/components/web/ConnectionStatusButton.js +++ b/react/features/video-menu/components/web/ConnectionStatusButton.js @@ -1,13 +1,12 @@ // @flow import React, { useCallback } from 'react'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; import { translate } from '../../../base/i18n'; import { IconInfo } from '../../../base/icons'; import { connect } from '../../../base/redux'; import { renderConnectionStatus } from '../../actions.web'; -import VideoMenuButton from './VideoMenuButton'; - type Props = { /** @@ -29,19 +28,19 @@ type Props = { const ConnectionStatusButton = ({ dispatch, - participantId, t }: Props) => { - const onClick = useCallback(() => { + const onClick = useCallback(e => { + e.stopPropagation(); dispatch(renderConnectionStatus(true)); }, [ dispatch ]); return ( - + onClick = { onClick } + text = { t('videothumbnail.connectionInfo') } /> ); }; diff --git a/react/features/video-menu/components/web/FlipLocalVideoButton.js b/react/features/video-menu/components/web/FlipLocalVideoButton.js index 1f86d1500..e357f5d2f 100644 --- a/react/features/video-menu/components/web/FlipLocalVideoButton.js +++ b/react/features/video-menu/components/web/FlipLocalVideoButton.js @@ -2,12 +2,11 @@ import React, { PureComponent } from 'react'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; import { translate } from '../../../base/i18n'; import { connect } from '../../../base/redux'; import { updateSettings } from '../../../base/settings'; -import VideoMenuButton from './VideoMenuButton'; - /** * The type of the React {@code Component} props of {@link FlipLocalVideoButton}. */ @@ -18,6 +17,11 @@ type Props = { */ _localFlipX: boolean, + /** + * Button text class name. + */ + className: string, + /** * The redux dispatch function. */ @@ -61,15 +65,18 @@ class FlipLocalVideoButton extends PureComponent { */ render() { const { + className, t } = this.props; return ( - + onClick = { this._onClick } + text = { t('videothumbnail.flip') } + textClassName = { className } /> ); } diff --git a/react/features/video-menu/components/web/GrantModeratorButton.js b/react/features/video-menu/components/web/GrantModeratorButton.js index a30b91089..ae3da5220 100644 --- a/react/features/video-menu/components/web/GrantModeratorButton.js +++ b/react/features/video-menu/components/web/GrantModeratorButton.js @@ -2,6 +2,7 @@ import React from 'react'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; import { translate } from '../../../base/i18n'; import { IconCrown } from '../../../base/icons'; import { connect } from '../../../base/redux'; @@ -10,8 +11,6 @@ import AbstractGrantModeratorButton, { type Props } from '../AbstractGrantModeratorButton'; -import VideoMenuButton from './VideoMenuButton'; - declare var interfaceConfig: Object; /** @@ -37,20 +36,20 @@ class GrantModeratorButton extends AbstractGrantModeratorButton { * @returns {ReactElement} */ render() { - const { participantID, t, visible } = this.props; + const { t, visible } = this.props; if (!visible) { return null; } return ( - + onClick = { this._handleClick } + text = { t('videothumbnail.grantModerator') } /> ); } diff --git a/react/features/video-menu/components/web/HideSelfViewVideoButton.js b/react/features/video-menu/components/web/HideSelfViewVideoButton.js index 7684c4f03..fa7bd6c89 100644 --- a/react/features/video-menu/components/web/HideSelfViewVideoButton.js +++ b/react/features/video-menu/components/web/HideSelfViewVideoButton.js @@ -2,14 +2,13 @@ import React, { PureComponent } from 'react'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; import { translate } from '../../../base/i18n'; import { connect } from '../../../base/redux'; import { updateSettings } from '../../../base/settings'; import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../../../notifications'; import { openSettingsDialog, SETTINGS_TABS } from '../../../settings'; -import VideoMenuButton from './VideoMenuButton'; - /** * The type of the React {@code Component} props of {@link HideSelfViewVideoButton}. */ @@ -25,6 +24,11 @@ type Props = { */ dispatch: Function, + /** + * Button text class name. + */ + className: string, + /** * Click handler executed aside from the main action. */ @@ -63,15 +67,18 @@ class HideSelfViewVideoButton extends PureComponent { */ render() { const { + className, t } = this.props; return ( - + ); } diff --git a/react/features/video-menu/components/web/KickButton.js b/react/features/video-menu/components/web/KickButton.js index 17fa8fd80..dbd5c8204 100644 --- a/react/features/video-menu/components/web/KickButton.js +++ b/react/features/video-menu/components/web/KickButton.js @@ -2,15 +2,14 @@ import React from 'react'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; import { translate } from '../../../base/i18n'; -import { IconKick } from '../../../base/icons'; +import { IconCloseCircle } from '../../../base/icons'; import { connect } from '../../../base/redux'; import AbstractKickButton, { type Props } from '../AbstractKickButton'; -import VideoMenuButton from './VideoMenuButton'; - /** * Implements a React {@link Component} which displays a button for kicking out * a participant from the conference. @@ -43,13 +42,14 @@ class KickButton extends AbstractKickButton { const { participantID, t } = this.props; return ( - + onClick = { this._handleClick } + text = { t('videothumbnail.kick') } /> ); } diff --git a/react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js b/react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js index 5b5b29b50..6aa2a2905 100644 --- a/react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js +++ b/react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js @@ -1,11 +1,14 @@ // @flow +import { withStyles } from '@material-ui/styles'; import React, { Component } from 'react'; import { batch } from 'react-redux'; +import ContextMenu from '../../../base/components/context-menu/ContextMenu'; +import ContextMenuItemGroup from '../../../base/components/context-menu/ContextMenuItemGroup'; import { isMobileBrowser } from '../../../base/environment/utils'; import { translate } from '../../../base/i18n'; -import { Icon, IconMenuThumb } from '../../../base/icons'; +import { Icon, IconHorizontalPoints } from '../../../base/icons'; import { getLocalParticipant } from '../../../base/participants'; @@ -20,8 +23,6 @@ import { renderConnectionStatus } from '../../actions.web'; import ConnectionStatusButton from './ConnectionStatusButton'; import FlipLocalVideoButton from './FlipLocalVideoButton'; import HideSelfViewVideoButton from './HideSelfViewVideoButton'; -import VideoMenu from './VideoMenu'; - /** * The type of the React {@code Component} props of @@ -30,29 +31,34 @@ import VideoMenu from './VideoMenu'; type Props = { /** - * The redux dispatch function. + * Whether or not the button should be visible. */ - dispatch: Function, + buttonVisible: boolean, /** - * Gets a ref to the current component instance. + * An object containing the CSS classes. */ - getRef: Function, + classes: Object, + + /** + * The redux dispatch function. + */ + dispatch: Function, /** * Hides popover. */ - hidePopover: Function, + hidePopover: Function, /** * Whether the popover is visible or not. */ - popoverVisible: boolean, + popoverVisible: boolean, /** * Shows popover. */ - showPopover: Function, + showPopover: Function, /** * The id of the local participant. @@ -87,6 +93,29 @@ type Props = { t: Function }; +const styles = theme => { + return { + triggerButton: { + backgroundColor: theme.palette.action01, + padding: '3px', + display: 'inline-block', + borderRadius: '4px' + }, + + contextMenu: { + position: 'relative', + marginTop: 0, + right: 'auto', + padding: '0', + minWidth: '200px' + }, + + flipText: { + marginLeft: '36px' + } + }; +}; + /** * React Component for displaying an icon associated with opening the * the video menu for the local participant. @@ -122,6 +151,8 @@ class LocalVideoMenuTriggerButton extends Component { _showConnectionInfo, _overflowDrawer, _showLocalVideoFlipButton, + buttonVisible, + classes, hidePopover, popoverVisible, t @@ -130,13 +161,22 @@ class LocalVideoMenuTriggerButton extends Component { const content = _showConnectionInfo ? : ( - - - - { isMobileBrowser() - && - } - + ); return ( @@ -149,14 +189,14 @@ class LocalVideoMenuTriggerButton extends Component { overflowDrawer = { _overflowDrawer } position = { _menuPosition } visible = { popoverVisible }> - {!_overflowDrawer && ( + {!_overflowDrawer && buttonVisible && ( + className = { classes.triggerButton } + role = 'button'> {!isMobileBrowser() && } @@ -221,10 +261,10 @@ function _mapStateToProps(state) { _menuPosition = 'left-start'; break; case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: - _menuPosition = 'left-end'; + _menuPosition = 'left-start'; break; case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: - _menuPosition = 'top'; + _menuPosition = 'top-start'; break; default: _menuPosition = 'auto'; @@ -239,4 +279,4 @@ function _mapStateToProps(state) { }; } -export default translate(connect(_mapStateToProps)(LocalVideoMenuTriggerButton)); +export default translate(connect(_mapStateToProps)(withStyles(styles)(LocalVideoMenuTriggerButton))); diff --git a/react/features/video-menu/components/web/MuteButton.js b/react/features/video-menu/components/web/MuteButton.js index 76f07da26..40b2e2b12 100644 --- a/react/features/video-menu/components/web/MuteButton.js +++ b/react/features/video-menu/components/web/MuteButton.js @@ -2,15 +2,15 @@ import React from 'react'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; import { translate } from '../../../base/i18n'; -import { IconMicDisabled } from '../../../base/icons'; +import { IconMicrophoneEmptySlash } from '../../../base/icons'; import { connect } from '../../../base/redux'; import AbstractMuteButton, { _mapStateToProps, type Props } from '../AbstractMuteButton'; -import VideoMenuButton from './VideoMenuButton'; /** * Implements a React {@link Component} which displays a button for audio muting @@ -41,23 +41,20 @@ class MuteButton extends AbstractMuteButton { * @returns {ReactElement} */ render() { - const { _audioTrackMuted, participantID, t } = this.props; - const muteConfig = _audioTrackMuted ? { - translationKey: 'videothumbnail.muted', - muteClassName: 'mutelink disabled' - } : { - translationKey: 'videothumbnail.domute', - muteClassName: 'mutelink' - }; + const { _audioTrackMuted, t } = this.props; + + if (_audioTrackMuted) { + return null; + } return ( - + onClick = { this._handleClick } + text = { t('dialog.muteParticipantButton') } /> ); } diff --git a/react/features/video-menu/components/web/MuteEveryoneElseButton.js b/react/features/video-menu/components/web/MuteEveryoneElseButton.js index f62608efd..4b51545b7 100644 --- a/react/features/video-menu/components/web/MuteEveryoneElseButton.js +++ b/react/features/video-menu/components/web/MuteEveryoneElseButton.js @@ -2,6 +2,7 @@ import React from 'react'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; import { translate } from '../../../base/i18n'; import { IconMuteEveryoneElse } from '../../../base/icons'; import { connect } from '../../../base/redux'; @@ -9,8 +10,6 @@ import AbstractMuteEveryoneElseButton, { type Props } from '../AbstractMuteEveryoneElseButton'; -import VideoMenuButton from './VideoMenuButton'; - /** * Implements a React {@link Component} which displays a button for audio muting * every participant in the conference except the one with the given @@ -35,16 +34,15 @@ class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton { * @returns {ReactElement} */ render() { - const { participantID, t } = this.props; + const { t } = this.props; return ( - + onClick = { this._handleClick } + text = { t('videothumbnail.domuteOthers') } /> ); } diff --git a/react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js b/react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js index 165a69fb9..6a264d516 100644 --- a/react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js +++ b/react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js @@ -2,6 +2,7 @@ import React from 'react'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; import { translate } from '../../../base/i18n'; import { IconMuteVideoEveryoneElse } from '../../../base/icons'; import { connect } from '../../../base/redux'; @@ -9,8 +10,6 @@ import AbstractMuteEveryoneElsesVideoButton, { type Props } from '../AbstractMuteEveryoneElsesVideoButton'; -import VideoMenuButton from './VideoMenuButton'; - /** * Implements a React {@link Component} which displays a button for audio muting * every participant in the conference except the one with the given @@ -35,16 +34,15 @@ class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton * @returns {ReactElement} */ render() { - const { participantID, t } = this.props; + const { t } = this.props; return ( - + onClick = { this._handleClick } + text = { t('videothumbnail.domuteVideoOfOthers') } /> ); } diff --git a/react/features/video-menu/components/web/MuteVideoButton.js b/react/features/video-menu/components/web/MuteVideoButton.js index 5a688ce33..864906964 100644 --- a/react/features/video-menu/components/web/MuteVideoButton.js +++ b/react/features/video-menu/components/web/MuteVideoButton.js @@ -2,16 +2,15 @@ import React from 'react'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; import { translate } from '../../../base/i18n'; -import { IconCameraDisabled } from '../../../base/icons'; +import { IconVideoOff } from '../../../base/icons'; import { connect } from '../../../base/redux'; import AbstractMuteVideoButton, { _mapStateToProps, type Props } from '../AbstractMuteVideoButton'; -import VideoMenuButton from './VideoMenuButton'; - /** * Implements a React {@link Component} which displays a button for disabling * the camera of a participant in the conference. @@ -41,23 +40,20 @@ class MuteVideoButton extends AbstractMuteVideoButton { * @returns {ReactElement} */ render() { - const { _videoTrackMuted, participantID, t } = this.props; - const muteConfig = _videoTrackMuted ? { - translationKey: 'videothumbnail.videoMuted', - muteClassName: 'mutevideolink disabled' - } : { - translationKey: 'videothumbnail.domuteVideo', - muteClassName: 'mutevideolink' - }; + const { _videoTrackMuted, t } = this.props; + + if (_videoTrackMuted) { + return null; + } return ( - + onClick = { this._handleClick } + text = { t('participantsPane.actions.stopVideo') } /> ); } diff --git a/react/features/video-menu/components/web/ParticipantContextMenu.js b/react/features/video-menu/components/web/ParticipantContextMenu.js new file mode 100644 index 000000000..edf97bed4 --- /dev/null +++ b/react/features/video-menu/components/web/ParticipantContextMenu.js @@ -0,0 +1,332 @@ +// @flow + +import { makeStyles } from '@material-ui/styles'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; + +import { Avatar } from '../../../base/avatar'; +import ContextMenu from '../../../base/components/context-menu/ContextMenu'; +import ContextMenuItemGroup from '../../../base/components/context-menu/ContextMenuItemGroup'; +import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils'; +import { IconShareVideo } from '../../../base/icons'; +import { MEDIA_TYPE } from '../../../base/media'; +import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants'; +import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions'; +import { setVolume } from '../../../filmstrip/actions.web'; +import { isForceMuted } from '../../../participants-pane/functions'; +import { requestRemoteControl, stopController } from '../../../remote-control'; +import { stopSharedVideo } from '../../../shared-video/actions.any'; +import { showOverflowDrawer } from '../../../toolbox/functions.web'; + +import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton'; +import SendToRoomButton from './SendToRoomButton'; + +import { + AskToUnmuteButton, + ConnectionStatusButton, + GrantModeratorButton, + MuteButton, + MuteEveryoneElseButton, + MuteEveryoneElsesVideoButton, + MuteVideoButton, + KickButton, + PrivateMessageMenuButton, + RemoteControlButton, + VolumeSlider +} from './'; + +type Props = { + + /** + * Class name for the context menu. + */ + className?: string, + + /** + * Closes a drawer if open. + */ + closeDrawer?: Function, + + /** + * The participant for which the drawer is open. + * It contains the displayName & participantID. + */ + drawerParticipant?: Object, + + /** + * Shared video local participant owner. + */ + localVideoOwner?: boolean, + + /** + * Target elements against which positioning calculations are made. + */ + offsetTarget?: HTMLElement, + + /** + * Callback for the mouse entering the component. + */ + onEnter?: Function, + + /** + * Callback for the mouse leaving the component. + */ + onLeave?: Function, + + /** + * Callback for making a selection in the menu. + */ + onSelect: Function, + + /** + * Participant reference. + */ + participant: Object, + + /** + * The current state of the participant's remote control session. + */ + remoteControlState?: number, + + /** + * Whether or not the menu is displayed in the thumbnail remote video menu. + */ + thumbnailMenu: ?boolean +} + +const useStyles = makeStyles(theme => { + return { + text: { + color: theme.palette.text02, + padding: '10px 16px', + height: '40px', + overflow: 'hidden', + display: 'flex', + alignItems: 'center', + boxSizing: 'border-box' + } + }; +}); + +const ParticipantContextMenu = ({ + className, + closeDrawer, + drawerParticipant, + localVideoOwner, + offsetTarget, + onEnter, + onLeave, + onSelect, + participant, + remoteControlState, + thumbnailMenu +}: Props) => { + const dispatch = useDispatch(); + const { t } = useTranslation(); + const styles = useStyles(); + + const localParticipant = useSelector(getLocalParticipant); + const _isModerator = Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR); + const _isAudioForceMuted = useSelector(state => + isForceMuted(participant, MEDIA_TYPE.AUDIO, state)); + const _isVideoForceMuted = useSelector(state => + isForceMuted(participant, MEDIA_TYPE.VIDEO, state)); + const _overflowDrawer = useSelector(showOverflowDrawer); + const { remoteVideoMenu = {}, disableRemoteMute, startSilent } + = useSelector(state => state['features/base/config']); + const { disableKick, disableGrantModerator } = remoteVideoMenu; + const { participantsVolume } = useSelector(state => state['features/filmstrip']); + const _volume = (participant?.local ?? true ? undefined + : participant?.id ? participantsVolume[participant?.id] : undefined) || 1; + + const _currentRoomId = useSelector(getCurrentRoomId); + const _rooms = Object.values(useSelector(getBreakoutRooms)); + + const _onVolumeChange = useCallback(value => { + dispatch(setVolume(participant.id, value)); + }, [ setVolume, dispatch ]); + + const clickHandler = useCallback(() => onSelect(true), [ onSelect ]); + + const _onStopSharedVideo = useCallback(() => { + clickHandler(); + dispatch(stopSharedVideo()); + }, [ stopSharedVideo ]); + + const _getCurrentParticipantId = useCallback(() => { + const drawer = _overflowDrawer && !thumbnailMenu; + + return (drawer ? drawerParticipant?.participantID : participant?.id) ?? ''; + } + , [ thumbnailMenu, _overflowDrawer, drawerParticipant, participant ]); + + const buttons = []; + const buttons2 = []; + + const showVolumeSlider = !startSilent + && !isIosMobileBrowser() + && (_overflowDrawer || thumbnailMenu) + && typeof _volume === 'number' + && !isNaN(_volume); + + const fakeParticipantActions = [ { + accessibilityLabel: t('toolbar.stopSharedVideo'), + icon: IconShareVideo, + onClick: _onStopSharedVideo, + text: t('toolbar.stopSharedVideo') + } ]; + + if (_isModerator) { + if (thumbnailMenu || _overflowDrawer) { + buttons.push( + ); + } + if (!disableRemoteMute) { + buttons.push( + + ); + buttons.push( + + ); + buttons.push( + + ); + buttons.push( + + ); + } + + if (!disableGrantModerator) { + buttons2.push( + + ); + } + + if (!disableKick) { + buttons2.push( + + ); + } + } + + buttons2.push( + + ); + + if (thumbnailMenu && isMobileBrowser()) { + buttons2.push( + + ); + } + + if (thumbnailMenu && remoteControlState) { + let onRemoteControlToggle = null; + + if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) { + onRemoteControlToggle = () => dispatch(stopController(true)); + } else if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) { + onRemoteControlToggle = () => dispatch(requestRemoteControl(_getCurrentParticipantId())); + } + + buttons2.push( + + ); + } + + const breakoutRoomsButtons = []; + + if (!thumbnailMenu && _isModerator) { + _rooms.forEach((room: Object) => { + if (room.id !== _currentRoomId) { + breakoutRoomsButtons.push( + + ); + } + }); + } + + return ( + + ); +}; + +export default ParticipantContextMenu; diff --git a/react/features/video-menu/components/web/PrivateMessageMenuButton.js b/react/features/video-menu/components/web/PrivateMessageMenuButton.js index 4d7270518..3a58e0afe 100644 --- a/react/features/video-menu/components/web/PrivateMessageMenuButton.js +++ b/react/features/video-menu/components/web/PrivateMessageMenuButton.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; import { translate } from '../../../base/i18n'; import { IconMessage } from '../../../base/icons'; import { connect } from '../../../base/redux'; @@ -12,8 +13,6 @@ import { } from '../../../chat/components/web/PrivateMessageButton'; import { isButtonEnabled } from '../../../toolbox/functions.web'; -import VideoMenuButton from './VideoMenuButton'; - declare var interfaceConfig: Object; type Props = AbstractProps & { @@ -49,18 +48,18 @@ class PrivateMessageMenuButton extends Component { * @returns {ReactElement} */ render() { - const { participantID, t, _hidden } = this.props; + const { t, _hidden } = this.props; if (_hidden) { return null; } return ( - + onClick = { this._onClick } + text = { t('toolbar.privateMessage') } /> ); } diff --git a/react/features/video-menu/components/web/RemoteControlButton.js b/react/features/video-menu/components/web/RemoteControlButton.js index 9602d0d23..31c5e4702 100644 --- a/react/features/video-menu/components/web/RemoteControlButton.js +++ b/react/features/video-menu/components/web/RemoteControlButton.js @@ -6,11 +6,10 @@ import { createRemoteVideoMenuButtonEvent, sendAnalytics } from '../../../analytics'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; import { translate } from '../../../base/i18n'; import { IconRemoteControlStart, IconRemoteControlStop } from '../../../base/icons'; -import VideoMenuButton from './VideoMenuButton'; - // TODO: Move these enums into the store after further reactification of the // non-react RemoteVideo component. export const REMOTE_CONTROL_MENU_STATES = { @@ -76,19 +75,18 @@ class RemoteControlButton extends Component { */ render() { const { - participantID, remoteControlState, t } = this.props; - let className, icon; + let disabled = false, icon; switch (remoteControlState) { case REMOTE_CONTROL_MENU_STATES.NOT_STARTED: icon = IconRemoteControlStart; break; case REMOTE_CONTROL_MENU_STATES.REQUESTING: - className = ' disabled'; + disabled = true; icon = IconRemoteControlStart; break; case REMOTE_CONTROL_MENU_STATES.STARTED: @@ -102,12 +100,13 @@ class RemoteControlButton extends Component { } return ( - + onClick = { this._onClick } + text = { t('videothumbnail.remoteControl') } /> ); } diff --git a/react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js b/react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js index 2e77a937f..8b73c9c55 100644 --- a/react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js +++ b/react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js @@ -1,39 +1,26 @@ // @flow /* eslint-disable react/jsx-handler-names */ +import { withStyles } from '@material-ui/styles'; import React, { Component } from 'react'; import { batch } from 'react-redux'; import ConnectionIndicatorContent from '../../../../features/connection-indicator/components/web/ConnectionIndicatorContent'; -import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils'; +import { isMobileBrowser } from '../../../base/environment/utils'; import { translate } from '../../../base/i18n'; -import { Icon, IconMenuThumb } from '../../../base/icons'; -import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants'; +import { Icon, IconHorizontalPoints } from '../../../base/icons'; +import { getParticipantById } from '../../../base/participants'; import { Popover } from '../../../base/popover'; import { connect } from '../../../base/redux'; import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actions'; -import { requestRemoteControl, stopController } from '../../../remote-control'; import { getCurrentLayout, LAYOUTS } from '../../../video-layout'; import { renderConnectionStatus } from '../../actions.web'; -import ConnectionStatusButton from './ConnectionStatusButton'; -import MuteEveryoneElseButton from './MuteEveryoneElseButton'; -import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton'; +import ParticipantContextMenu from './ParticipantContextMenu'; import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton'; -import { - GrantModeratorButton, - MuteButton, - MuteVideoButton, - KickButton, - PrivateMessageMenuButton, - RemoteControlButton, - VideoMenu, - VolumeSlider -} from './'; - declare var $: Object; /** @@ -45,37 +32,17 @@ type Props = { /** * Hides popover. */ - hidePopover: Function, + hidePopover: Function, /** * Whether the popover is visible or not. */ - popoverVisible: boolean, + popoverVisible: boolean, /** * Shows popover. */ - showPopover: Function, - - /** - * Whether or not to display the kick button. - */ - _disableKick: boolean, - - /** - * Whether or not to display the remote mute buttons. - */ - _disableRemoteMute: Boolean, - - /** - * Whether or not to display the grant moderator button. - */ - _disableGrantModerator: Boolean, - - /** - * Whether or not the participant is a conference moderator. - */ - _isModerator: boolean, + showPopover: Function, /** * The position relative to the trigger the remote menu should display @@ -89,33 +56,31 @@ type Props = { */ _overflowDrawer: boolean, + /** + * Participant reference. + */ + _participant: Object, + /** * The current state of the participant's remote control session. */ _remoteControlState: number, + /** + * Whether or not the button should be visible. + */ + buttonVisible: boolean, + + /** + * An object containing the CSS classes. + */ + classes: Object, + /** * The redux dispatch function. */ dispatch: Function, - /** - * Gets a ref to the current component instance. - */ - getRef: Function, - - /** - * A value between 0 and 1 indicating the volume of the participant's - * audio element. - */ - initialVolumeValue: ?number, - - /** - * Callback to invoke when changing the level of the participant's - * audio element. - */ - onVolumeChange: Function, - /** * The ID for the participant on which the remote video menu will act. */ @@ -137,6 +102,26 @@ type Props = { t: Function }; +const styles = theme => { + return { + triggerButton: { + backgroundColor: theme.palette.action01, + padding: '3px', + display: 'inline-block', + borderRadius: '4px' + }, + + contextMenu: { + position: 'relative', + marginTop: 0, + right: 'auto', + padding: '0', + marginRight: '4px', + marginBottom: '4px' + } + }; +}; + /** * React {@code Component} for displaying an icon associated with opening the * the {@code VideoMenu}. @@ -169,6 +154,8 @@ class RemoteVideoMenuTriggerButton extends Component { _overflowDrawer, _showConnectionInfo, _participantDisplayName, + buttonVisible, + classes, participantID, popoverVisible } = this.props; @@ -190,13 +177,14 @@ class RemoteVideoMenuTriggerButton extends Component { onPopoverOpen = { this._onPopoverOpen } position = { this.props._menuPosition } visible = { popoverVisible }> - {!_overflowDrawer && ( - + {!_overflowDrawer && buttonVisible && ( + {!isMobileBrowser() && } @@ -245,133 +233,16 @@ class RemoteVideoMenuTriggerButton extends Component { * @returns {ReactElement} */ _renderRemoteVideoMenu() { - const { - _disableKick, - _disableRemoteMute, - _disableGrantModerator, - _isModerator, - dispatch, - initialVolumeValue, - onVolumeChange, - _remoteControlState, - participantID - } = this.props; + const { _participant, _remoteControlState, classes } = this.props; - const actions = []; - const buttons = []; - const showVolumeSlider = !isIosMobileBrowser() - && onVolumeChange - && typeof initialVolumeValue === 'number' - && !isNaN(initialVolumeValue); - - if (_isModerator) { - if (!_disableRemoteMute) { - buttons.push( - - ); - buttons.push( - - ); - buttons.push( - - ); - buttons.push( - - ); - } - - if (!_disableGrantModerator) { - buttons.push( - - ); - } - - if (!_disableKick) { - buttons.push( - - ); - } - } - - if (_remoteControlState) { - let onRemoteControlToggle = null; - - if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) { - onRemoteControlToggle = () => dispatch(stopController(true)); - } else if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) { - onRemoteControlToggle = () => dispatch(requestRemoteControl(participantID)); - } - - buttons.push( - - ); - } - - buttons.push( - + return ( + ); - - if (isMobileBrowser()) { - actions.push( - - ); - } - - if (showVolumeSlider) { - actions.push( - - ); - } - - if (buttons.length > 0 || actions.length > 0) { - return ( - - <> - { buttons.length > 0 - &&
  • -
      - { buttons } -
    -
  • - } - - <> - { actions.length > 0 - &&
  • -
      - {actions} -
    -
  • - } - -
    - ); - } - - return null; } } @@ -385,9 +256,6 @@ class RemoteVideoMenuTriggerButton extends Component { */ function _mapStateToProps(state, ownProps) { const { participantID } = ownProps; - const localParticipant = getLocalParticipant(state); - const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config']; - const { disableKick, disableGrantModerator } = remoteVideoMenu; let _remoteControlState = null; const participant = getParticipantById(state, participantID); const _participantDisplayName = participant?.name; @@ -428,17 +296,14 @@ function _mapStateToProps(state, ownProps) { } return { - _isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR), - _disableKick: Boolean(disableKick), - _disableRemoteMute: Boolean(disableRemoteMute), - _remoteControlState, _menuPosition, _overflowDrawer: overflowDrawer, + _participant: participant, _participantDisplayName, - _disableGrantModerator: Boolean(disableGrantModerator), + _remoteControlState, _showConnectionInfo: showConnectionInfo }; } -export default translate(connect(_mapStateToProps)(RemoteVideoMenuTriggerButton)); -/* eslint-enable react/jsx-handler-names */ +export default translate(connect(_mapStateToProps)( + withStyles(styles)(RemoteVideoMenuTriggerButton))); diff --git a/react/features/video-menu/components/web/SendToRoomButton.js b/react/features/video-menu/components/web/SendToRoomButton.js new file mode 100644 index 000000000..a1f100523 --- /dev/null +++ b/react/features/video-menu/components/web/SendToRoomButton.js @@ -0,0 +1,50 @@ +// @flow + +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; + +import { createBreakoutRoomsEvent, sendAnalytics } from '../../../analytics'; +import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem'; +import { IconRingGroup } from '../../../base/icons'; +import { sendParticipantToRoom } from '../../../breakout-rooms/actions'; + +type Props = { + + /** + * Click handler. + */ + onClick: ?Function, + + /** + * The ID for the participant on which the button will act. + */ + participantID: string, + + /** + * The room to send the participant to. + */ + room: Object +} + +const SendToRoomButton = ({ onClick, participantID, room }: Props) => { + const dispatch = useDispatch(); + const { t } = useTranslation(); + const _onClick = useCallback(() => { + onClick && onClick(); + sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room')); + dispatch(sendParticipantToRoom(participantID, room.id)); + }, [ participantID, room ]); + + const roomName = room.name || t('breakoutRooms.mainRoom'); + + return ( + + ); +}; + +export default SendToRoomButton; diff --git a/react/features/video-menu/components/web/VideoMenu.js b/react/features/video-menu/components/web/VideoMenu.js deleted file mode 100644 index 6924428f3..000000000 --- a/react/features/video-menu/components/web/VideoMenu.js +++ /dev/null @@ -1,51 +0,0 @@ -// @flow - -import React from 'react'; - -/** - * The type of the React {@code Component} props of {@link VideoMenu}. - */ -type Props = { - - /** - * The components to place as the body of the {@code VideoMenu}. - */ - children: React$Node, - - /** - * The id attribute to be added to the component's DOM for retrieval when - * querying the DOM. Not used directly by the component. - */ - id: string -}; - -/** - * Click handler. - * - * @param {SyntheticEvent} event - The click event. - * @returns {void} - */ -function onClick(event) { - // If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation - // needs to be stopped. - event.stopPropagation(); -} - -/** - * React {@code Component} responsible for displaying other components as a menu - * for manipulating participant state. - * - * @param {Props} props - The component's props. - * @returns {Component} - */ -export default function VideoMenu(props: Props) { - return ( -
      - { props.children } -
    - ); -} - diff --git a/react/features/video-menu/components/web/VideoMenuButton.js b/react/features/video-menu/components/web/VideoMenuButton.js deleted file mode 100644 index 2e32ad3c0..000000000 --- a/react/features/video-menu/components/web/VideoMenuButton.js +++ /dev/null @@ -1,111 +0,0 @@ -/* @flow */ - -import React, { Component } from 'react'; - -import { Icon } from '../../../base/icons'; - -/** - * The type of the React {@code Component} props of - * {@link VideoMenuButton}. - */ -type Props = { - - /** - * Text to display within the component that describes the onClick action. - */ - buttonText: string, - - /** - * Additional CSS classes to add to the component. - */ - displayClass?: string, - - /** - * The icon that will display within the component. - */ - icon?: Object, - - /** - * The id attribute to be added to the component's DOM for retrieval when - * querying the DOM. Not used directly by the component. - */ - id: string, - - /** - * Callback to invoke when the component is clicked. - */ - onClick: Function, -}; - -/** - * React {@code Component} for displaying an action in {@code VideoMenuButton}. - * - * @augments {Component} - */ -export default class VideoMenuButton extends Component { - /** - * Initializes a new {@code RemoteVideoMenuButton} instance. - * - * @param {*} props - The read-only properties with which the new instance - * is to be initialized. - */ - constructor(props: Props) { - super(props); - - // Bind event handler so it is only bound once for every instance. - this._onKeyPress = this._onKeyPress.bind(this); - } - - _onKeyPress: (Object) => void; - - /** - * KeyPress handler for accessibility. - * - * @param {Object} e - The synthetic event. - * @returns {void} - */ - _onKeyPress(e) { - if (this.props.onClick && (e.key === ' ' || e.key === 'Enter')) { - e.preventDefault(); - this.props.onClick(); - } - } - - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { - buttonText, - displayClass, - icon, - id, - onClick - } = this.props; - - const linkClassName = `popupmenu__link ${displayClass || ''}`; - - return ( -
  • - - - { icon && } - - - { buttonText } - - -
  • - ); - } -} diff --git a/react/features/video-menu/components/web/VolumeSlider.js b/react/features/video-menu/components/web/VolumeSlider.js index 92e6564b0..4198bb768 100644 --- a/react/features/video-menu/components/web/VolumeSlider.js +++ b/react/features/video-menu/components/web/VolumeSlider.js @@ -1,5 +1,7 @@ /* @flow */ +import { withStyles } from '@material-ui/styles'; +import clsx from 'clsx'; import React, { Component } from 'react'; import { translate } from '../../../base/i18n'; @@ -11,6 +13,11 @@ import { VOLUME_SLIDER_SCALE } from '../../constants'; */ type Props = { + /** + * An object containing the CSS classes. + */ + classes: Object, + /** * The value of the audio slider should display at when the component first * mounts. Changes will be stored in state. The value should be a number @@ -41,6 +48,43 @@ type State = { volumeLevel: number }; +const styles = theme => { + return { + container: { + minHeight: '40px', + width: '100%', + boxSizing: 'border-box', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + padding: '0 5px', + + '&:hover': { + backgroundColor: theme.palette.ui04 + } + }, + + icon: { + minWidth: '20px', + padding: '5px', + position: 'relative' + }, + + sliderContainer: { + position: 'relative', + width: '100%', + paddingRight: '5px' + }, + + slider: { + position: 'absolute', + width: '100%', + top: '50%', + transform: 'translate(0, -50%)' + } + }; +}; + /** * Implements a React {@link Component} which displays an input slider for * adjusting the local volume of a remote participant. @@ -65,6 +109,16 @@ class VolumeSlider extends Component { this._onVolumeChange = this._onVolumeChange.bind(this); } + /** + * Click handler. + * + * @param {MouseEvent} e - Click event. + * @returns {void} + */ + _onClick(e) { + e.stopPropagation(); + } + /** * Implements React's {@link Component#render()}. * @@ -72,29 +126,32 @@ class VolumeSlider extends Component { * @returns {ReactElement} */ render() { + const { classes } = this.props; + return ( -
  • -
    - - - -
    - -
    + className = { clsx('popupmenu__contents', classes.container) } + onClick = { this._onClick }> + + + +
    +
    -
  • + ); } @@ -116,4 +173,4 @@ class VolumeSlider extends Component { } } -export default translate(VolumeSlider); +export default translate(withStyles(styles)(VolumeSlider)); diff --git a/react/features/video-menu/components/web/index.js b/react/features/video-menu/components/web/index.js index a700adfa6..a258e02e0 100644 --- a/react/features/video-menu/components/web/index.js +++ b/react/features/video-menu/components/web/index.js @@ -1,5 +1,6 @@ // @flow +export { default as AskToUnmuteButton } from './AskToUnmuteButton'; export { default as ConnectionStatusButton } from './ConnectionStatusButton'; export { default as GrantModeratorButton } from './GrantModeratorButton'; export { default as GrantModeratorDialog } from './GrantModeratorDialog'; @@ -14,7 +15,6 @@ export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVide export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog'; export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton'; export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton'; -export { default as VideoMenu } from './VideoMenu'; export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton'; export { default as LocalVideoMenuTriggerButton } from './LocalVideoMenuTriggerButton'; export { default as VolumeSlider } from './VolumeSlider';