feat(participants-pane) implement participants pane

This commit is contained in:
Gabriel Imre 2021-04-21 16:48:05 +03:00 committed by GitHub
parent 6efa94541e
commit d014a52ab3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1549 additions and 81 deletions

View File

@ -0,0 +1,51 @@
.participants_pane {
background-color: $participantsPaneBgColor;
flex-shrink: 0;
overflow: hidden;
position: relative;
transition: width .16s ease-in-out;
width: 315px;
z-index: $zindex0;
&--closed {
width: 0;
}
}
.participants_pane-content {
display: flex;
flex-direction: column;
font-weight: 600;
height: 100%;
width: 315px;
& > *:first-child,
& > *:last-child {
flex-shrink: 0;
}
}
.participant-avatar {
margin: 8px 16px 8px 0;
}
@media (max-width: 375px) {
.participants_pane {
height: 100vh;
height: -webkit-fill-available;
left: 0;
position: fixed;
right: 0;
top: 0;
width: auto;
&--closed {
display: none;
width: auto;
}
}
.participants_pane-content {
width: 100%;
}
}

View File

@ -30,6 +30,7 @@ $defaultSideBarFontColor: #44A5FF;
$defaultSemiDarkColor: #ACACAC;
$defaultDarkColor: #2b3d5c;
$defaultWarningColor: rgb(215, 121, 118);
$participantsPaneBgColor: #141414;
$presence-available: rgb(110, 176, 5);
$presence-away: rgb(250, 201, 20);
$presence-busy: rgb(233, 0, 27);

View File

@ -1,5 +1,13 @@
#videoconference_page {
min-height: 100%;
position: relative;
transform: translate3d(0, 0, 0);
width: 100%;
}
#layout_wrapper {
display: flex;
height: 100%;
}
#videospace {

View File

@ -1,3 +1,5 @@
$rangeInputThumbSize: 14;
/**
* Disable the default webkit styles for range inputs (sliders).
*/

View File

@ -16,7 +16,7 @@
display: flex;
flex-direction: column;
height: 100%;
width: 100vw;
width: 100%;
}
.filmstrip__videos .videocontainer {
@ -50,10 +50,6 @@
&.shift-right {
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
#filmstripRemoteVideos {
width: calc(100vw - #{$sidebarWidth});
}
}
}
}

View File

@ -104,5 +104,6 @@ $flagsImagePath: "../images/";
@import 'responsive';
@import 'connection-status';
@import 'drawer';
@import 'participants-pane';
/* Modules END */

View File

@ -418,6 +418,7 @@
"showSpeakerStats": "Show speaker stats",
"toggleChat": "Open or close the chat",
"toggleFilmstrip": "Show or hide video thumbnails",
"toggleParticipantsPane": "Show or hide the participants pane",
"toggleScreensharing": "Switch between camera and screen sharing",
"toggleShortcuts": "Show or hide keyboard shortcuts",
"videoMute": "Start or stop your camera"
@ -527,6 +528,16 @@
"oldElectronClientDescription2": "latest build",
"oldElectronClientDescription3": " now!"
},
"participantsPane": {
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Meeting participants ({{count}})"
},
"actions": {
"muteAll": "Mute all",
"stopVideo": "Stop video"
}
},
"passwordSetRemotely": "Set by another participant",
"passwordDigitsOnly": "Up to {{number}} digits",
"poweredby": "powered by",
@ -745,6 +756,7 @@
"muteEveryoneElse": "Mute everyone else",
"muteEveryonesVideo": "Disable everyone's camera",
"muteEveryoneElsesVideo": "Disable everyone else's camera",
"participants": "Participants",
"pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
@ -807,6 +819,7 @@
"noisyAudioInputTitle": "Your microphone appears to be noisy!",
"noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
"openChat": "Open chat",
"participants": "Participants",
"pip": "Enter Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
@ -942,6 +955,7 @@
"header": "Help center"
},
"lobby": {
"admit": "Admit",
"knockingParticipantList": "Knocking participant list",
"allow": "Allow",
"backToKnockModeButton": "No password, ask to join instead",

View File

@ -19,6 +19,8 @@ import { CHAT_SIZE } from '../../../react/features/chat';
import {
updateKnownLargeVideoResolution
} from '../../../react/features/large-video/actions';
import { getParticipantsPaneOpen } from '../../../react/features/participants-pane/functions';
import theme from '../../../react/features/participants-pane/theme.json';
import { PresenceLabel } from '../../../react/features/presence-status';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
@ -366,7 +368,13 @@ export default class LargeVideoManager {
}
let widthToUse = this.preferredWidth || window.innerWidth;
const { isOpen } = APP.store.getState()['features/chat'];
const state = APP.store.getState();
const { isOpen } = state['features/chat'];
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
if (isParticipantsPaneOpen) {
widthToUse -= theme.participantsPaneWidth;
}
if (isOpen && window.innerWidth > 580) {
/**

View File

@ -48,10 +48,10 @@
"i18next": "17.0.6",
"i18next-browser-languagedetector": "3.0.1",
"i18next-xhr-backend": "3.0.0",
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
"jquery": "3.5.1",
"jquery-i18next": "1.2.1",
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0dc1540a44131d4c287993d2cfe0ed8e5ae70d4c",

View File

@ -6,6 +6,7 @@ import '../feedback/reducer';
import '../local-recording/reducer';
import '../no-audio-signal/reducer';
import '../noise-detection/reducer';
import '../participants-pane/reducer';
import '../power-monitor/reducer';
import '../prejoin/reducer';
import '../remote-control/reducer';

View File

@ -20,6 +20,22 @@ import {
} from './constants';
import logger from './logger';
/**
* Returns root conference state.
*
* @param {Object} state - Global state.
* @returns {Object} Conference state.
*/
export const getConferenceState = (state: Object) => state['features/base/conference'];
/**
* Is the conference joined or not.
*
* @param {Object} state - Global state.
* @returns {boolean}
*/
export const getIsConferenceJoined = (state: Object) => Boolean(getConferenceState(state).conference);
/**
* Attach a set of local tracks to a conference.
*
@ -123,7 +139,7 @@ export function commonUserLeftHandling(
export function forEachConference(
stateful: Function | Object,
predicate: (Object, URL) => boolean) {
const state = toState(stateful)['features/base/conference'];
const state = getConferenceState(toState(stateful));
for (const v of Object.values(state)) {
// Does the value of the base/conference's property look like a
@ -157,7 +173,7 @@ export function getConferenceName(stateful: Function | Object): string {
const state = toState(stateful);
const { callee } = state['features/base/jwt'];
const { callDisplayName } = state['features/base/config'];
const { pendingSubjectChange, room, subject } = state['features/base/conference'];
const { pendingSubjectChange, room, subject } = getConferenceState(state);
return pendingSubjectChange
|| subject
@ -174,7 +190,7 @@ export function getConferenceName(stateful: Function | Object): string {
* @returns {string} - The name of the conference formatted for the title.
*/
export function getConferenceNameForTitle(stateful: Function | Object) {
return safeStartCase(safeDecodeURIComponent(toState(stateful)['features/base/conference'].room));
return safeStartCase(safeDecodeURIComponent(getConferenceState(toState(stateful)).room));
}
/**
@ -186,7 +202,7 @@ export function getConferenceNameForTitle(stateful: Function | Object) {
*/
export function getConferenceTimestamp(stateful: Function | Object): number {
const state = toState(stateful);
const { conferenceTimestamp } = state['features/base/conference'];
const { conferenceTimestamp } = getConferenceState(state);
return conferenceTimestamp;
}
@ -203,7 +219,7 @@ export function getConferenceTimestamp(stateful: Function | Object): number {
*/
export function getCurrentConference(stateful: Function | Object) {
const { conference, joining, leaving, membersOnly, passwordRequired }
= toState(stateful)['features/base/conference'];
= getConferenceState(toState(stateful));
// There is a precedence
if (conference) {
@ -220,7 +236,7 @@ export function getCurrentConference(stateful: Function | Object) {
* @returns {string}
*/
export function getRoomName(state: Object): string {
return state['features/base/conference'].room;
return getConferenceState(state).room;
}
/**

View File

@ -17,7 +17,7 @@ export const TOOLBAR_BUTTONS = [
'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'shareaudio', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'videoquality', 'filmstrip', 'participants-pane', 'feedback', 'stats', 'shortcuts',
'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone',
'security', 'toggle-camera'
];

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0001 16.6666C13.682 16.6666 16.6667 13.6819 16.6667 9.99996C16.6667 6.31806 13.682 3.33329 10.0001 3.33329C6.31818 3.33329 3.33341 6.31806 3.33341 9.99996C3.33341 13.6819 6.31818 16.6666 10.0001 16.6666ZM10.0001 18.3333C5.39771 18.3333 1.66675 14.6023 1.66675 9.99996C1.66675 5.39759 5.39771 1.66663 10.0001 1.66663C14.6025 1.66663 18.3334 5.39759 18.3334 9.99996C18.3334 14.6023 14.6025 18.3333 10.0001 18.3333ZM10.0001 8.82145L12.3571 6.46443C12.6825 6.13899 13.2102 6.13899 13.5356 6.46443C13.8611 6.78986 13.8611 7.3175 13.5356 7.64294L11.1786 9.99996L13.5356 12.357C13.8611 12.6824 13.8611 13.2101 13.5356 13.5355C13.2102 13.8609 12.6825 13.8609 12.3571 13.5355L10.0001 11.1785L7.64306 13.5355C7.31762 13.8609 6.78998 13.8609 6.46455 13.5355C6.13911 13.2101 6.13911 12.6824 6.46455 12.357L8.82157 9.99996L6.46455 7.64294C6.13911 7.3175 6.13911 6.78986 6.46455 6.46443C6.78998 6.13899 7.31762 6.13899 7.64306 6.46443L10.0001 8.82145Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -27,6 +27,7 @@ export { default as IconChatSend } from './send.svg';
export { default as IconChatUnread } from './chat-unread.svg';
export { default as IconCheck } from './check.svg';
export { default as IconClose } from './close.svg';
export { default as IconCloseCircle } from './close-circle.svg';
export { default as IconCloseX } from './close-x.svg';
export { default as IconClosedCaption } from './closed_caption.svg';
export { default as IconCloseSmall } from './close-small.svg';
@ -68,10 +69,13 @@ export { default as IconMenuThumb } from './thumb-menu.svg';
export { default as IconMenuUp } from './menu-up.svg';
export { default as IconMessage } from './message.svg';
export { default as IconMeter } from './meter.svg';
export { default as IconMicBlockedHollow } from './mic-blocked.svg';
export { default as IconMicDisabled } from './mic-disabled.svg';
export { default as IconMicDisabledHollow } from './mic-disabled-hollow.svg';
export { default as IconMicrophone } from './microphone.svg';
export { default as IconMicrophoneEmpty } from './microphone-empty.svg';
export { default as IconMicrophoneEmptySlash } from './microphone-empty-slash.svg';
export { default as IconMicrophoneHollow } from './microphone-hollow.svg';
export { default as IconModerator } from './star.svg';
export { default as IconMuteEveryone } from './mute-everyone.svg';
export { default as IconMuteEveryoneElse } from './mute-everyone-else.svg';
@ -80,11 +84,13 @@ export { default as IconMuteVideoEveryoneElse } from './mute-video-everyone-else
export { default as IconNotificationJoin } from './navigate_next.svg';
export { default as IconOpenInNew } from './open_in_new.svg';
export { default as IconOutlook } from './office365.svg';
export { default as IconParticipants } from './participants.svg';
export { default as IconPhone } from './phone.svg';
export { default as IconPin } from './enlarge.svg';
export { default as IconPlane } from './paper-plane.svg';
export { default as IconPresentation } from './presentation.svg';
export { default as IconRaisedHand } from './raised-hand.svg';
export { default as IconRaisedHandHollow } from './raised-hand-hollow.svg';
export { default as IconRec } from './rec.svg';
export { default as IconRemoteControlStart } from './play.svg';
export { default as IconRemoteControlStop } from './stop.svg';
@ -109,6 +115,7 @@ export { default as IconSwitchCamera } from './switch-camera.svg';
export { default as IconTileView } from './tiles-many.svg';
export { default as IconToggleRecording } from './camera-take-picture.svg';
export { default as IconTrash } from './trash.svg';
export { default as IconVideoOff } from './video-off.svg';
export { default as IconVideoQualityAudioOnly } from './AUD.svg';
export { default as IconVideoQualityHD } from './HD.svg';
export { default as IconVideoQualityLD } from './LD.svg';

View File

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1667 18.3598C14.627 18.3598 15.0001 17.9867 15.0001 17.5265V13.3333H17.5001C17.9603 13.3333 18.3334 12.9602 18.3334 12.5V2.49996C18.3334 2.03972 17.9603 1.66663 17.5001 1.66663H2.50008C2.03984 1.66663 1.66675 2.03972 1.66675 2.49996V12.5C1.66675 12.9602 2.03984 13.3333 2.50008 13.3333H9.62979L13.5238 18.0566C13.6821 18.2486 13.9179 18.3598 14.1667 18.3598ZM3.33341 3.33329H16.6667V11.6666H13.3334V15.2057L10.4158 11.6666H3.33341V3.33329Z" />
</svg>

Before

Width:  |  Height:  |  Size: 256 B

After

Width:  |  Height:  |  Size: 588 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99992 1.66669C11.8409 1.66669 13.3333 3.15907 13.3333 5.00002V10C13.3333 11.5555 12.2678 12.8621 10.8269 13.23C10.8311 13.2638 10.8333 13.2983 10.8333 13.3334V14.9309C11.1452 14.8785 11.4474 14.7973 11.7369 14.69C12.0292 13.2018 13.2017 12.0293 14.6899 11.737C14.8904 11.196 14.9999 10.6108 14.9999 10C14.9999 9.53978 15.373 9.16669 15.8333 9.16669C16.2935 9.16669 16.6666 9.53978 16.6666 10C16.6666 10.6246 16.5807 11.2292 16.4201 11.8025C18.0039 12.2412 19.1666 13.6932 19.1666 15.4167C19.1666 17.4878 17.4877 19.1667 15.4166 19.1667C13.6931 19.1667 12.2411 18.004 11.8024 16.4202C11.4881 16.5082 11.1644 16.5738 10.8333 16.6151V17.5C10.8333 17.9603 10.4602 18.3334 9.99992 18.3334C9.53968 18.3334 9.16659 17.9603 9.16659 17.5V16.6151C5.87799 16.205 3.33325 13.3997 3.33325 10C3.33325 9.53978 3.70635 9.16669 4.16659 9.16669C4.62682 9.16669 4.99992 9.53978 4.99992 10C4.99992 12.4775 6.80182 14.5342 9.16659 14.9309V13.3334C9.16659 13.2983 9.16875 13.2638 9.17294 13.23C7.73203 12.8621 6.66659 11.5555 6.66659 10V5.00002C6.66659 3.15907 8.15897 1.66669 9.99992 1.66669ZM9.99992 3.33335C9.07944 3.33335 8.33325 4.07955 8.33325 5.00002V10C8.33325 10.9205 9.07944 11.6667 9.99992 11.6667C10.9204 11.6667 11.6666 10.9205 11.6666 10V5.00002C11.6666 4.07955 10.9204 3.33335 9.99992 3.33335ZM13.3333 15V15.8334H17.4999V15H13.3333Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 7.07804V9C6 10.3999 6.9589 11.5759 8.25572 11.907C8.25195 11.9374 8.25 11.9685 8.25 12V13.4378C6.12171 13.0807 4.5 11.2297 4.5 9C4.5 8.58579 4.16421 8.25 3.75 8.25C3.33579 8.25 3 8.58579 3 9C3 12.0597 5.29027 14.5845 8.25 14.9536V15.75C8.25 16.1642 8.58579 16.5 9 16.5C9.41421 16.5 9.75 16.1642 9.75 15.75V14.9536C10.8412 14.8175 11.8415 14.3884 12.6694 13.7475L15.1986 16.2766C15.4964 16.5744 15.9791 16.5745 16.2768 16.2768C16.5745 15.9791 16.5744 15.4964 16.2766 15.1986L13.7475 12.6694C13.7502 12.6659 13.753 12.6623 13.7557 12.6588L12.6831 11.5861C12.6805 11.5898 12.6779 11.5935 12.6753 11.5972L11.5911 10.513C11.5934 10.5091 11.5957 10.5051 11.598 10.5011L10.4566 9.35965C10.4554 9.3647 10.4541 9.36974 10.4528 9.37476L7.5 6.42196V6.40304L6 4.90304V4.92196L2.80143 1.72339C2.50364 1.4256 2.02091 1.42553 1.72322 1.72322C1.42553 2.02091 1.4256 2.50364 1.72339 2.80143L6 7.07804ZM7.5 8.57804V9C7.5 9.82843 8.17157 10.5 9 10.5C9.1294 10.5 9.25498 10.4836 9.37476 10.4528L7.5 8.57804ZM10.513 11.5911C10.2756 11.73 10.0175 11.8372 9.74428 11.907C9.74805 11.9374 9.75 11.9685 9.75 12V13.4378C10.4295 13.3238 11.0573 13.0575 11.5972 12.6753L10.513 11.5911ZM12 8.74696L10.5 7.24696V4.5C10.5 3.67157 9.82843 3 9 3C8.25144 3 7.63095 3.54832 7.51827 4.26522L6.34845 3.09541C6.85223 2.14635 7.85064 1.5 9 1.5C10.6569 1.5 12 2.84315 12 4.5V8.74696ZM13.3623 10.1092L14.5462 11.2932C14.8386 10.5867 15 9.81218 15 9C15 8.58579 14.6642 8.25 14.25 8.25C13.8358 8.25 13.5 8.58579 13.5 9C13.5 9.38278 13.4522 9.7544 13.3623 10.1092Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4.5C12 2.84315 10.6569 1.5 9 1.5C7.34315 1.5 6 2.84315 6 4.5V9C6 10.3999 6.9589 11.5759 8.25572 11.907C8.25195 11.9374 8.25 11.9685 8.25 12V13.4378C6.12171 13.0807 4.5 11.2297 4.5 9C4.5 8.58579 4.16421 8.25 3.75 8.25C3.33579 8.25 3 8.58579 3 9C3 12.0597 5.29027 14.5845 8.25 14.9536V15.75C8.25 16.1642 8.58579 16.5 9 16.5C9.41421 16.5 9.75 16.1642 9.75 15.75V14.9536C12.7097 14.5845 15 12.0597 15 9C15 8.58579 14.6642 8.25 14.25 8.25C13.8358 8.25 13.5 8.58579 13.5 9C13.5 11.2297 11.8783 13.0807 9.75 13.4378V12C9.75 11.9685 9.74805 11.9374 9.74428 11.907C11.0411 11.5759 12 10.3999 12 9V4.5ZM9 3C8.17157 3 7.5 3.67157 7.5 4.5V9C7.5 9.82843 8.17157 10.5 9 10.5C9.82843 10.5 10.5 9.82843 10.5 9V4.5C10.5 3.67157 9.82843 3 9 3Z" />
</svg>

After

Width:  |  Height:  |  Size: 886 B

View File

@ -1,4 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 13.078V15C6 16.3999 6.9589 17.5759 8.25572 17.907C8.25195 17.9374 8.25 17.9685 8.25 18V19.4378C6.12171 19.0807 4.5 17.2297 4.5 15C4.5 14.5858 4.16421 14.25 3.75 14.25C3.33579 14.25 3 14.5858 3 15C3 18.0597 5.29027 20.5845 8.25 20.9536V21.75C8.25 22.1642 8.58579 22.5 9 22.5C9.41421 22.5 9.75 22.1642 9.75 21.75V20.9536C10.8412 20.8175 11.8415 20.3884 12.6694 19.7475L15.1986 22.2766C15.4964 22.5744 15.9791 22.5745 16.2768 22.2768C16.5745 21.9791 16.5744 21.4964 16.2766 21.1986L13.7475 18.6694C13.7502 18.6659 13.753 18.6623 13.7557 18.6588L12.6831 17.5861C12.6805 17.5898 12.6779 17.5935 12.6753 17.5972L11.5911 16.513C11.5934 16.5091 11.5957 16.5051 11.598 16.5011L10.4566 15.3596C10.4554 15.3647 10.4541 15.3697 10.4528 15.3748L7.5 12.422V12.403L6 10.903V10.922L2.80143 7.72339C2.50364 7.4256 2.02091 7.42553 1.72322 7.72322C1.42553 8.02091 1.4256 8.50364 1.72339 8.80143L6 13.078ZM7.5 14.578V15C7.5 15.8284 8.17157 16.5 9 16.5C9.1294 16.5 9.25498 16.4836 9.37476 16.4528L7.5 14.578ZM10.513 17.5911C10.2756 17.73 10.0175 17.8372 9.74428 17.907C9.74805 17.9374 9.75 17.9685 9.75 18V19.4378C10.4295 19.3238 11.0573 19.0575 11.5972 18.6753L10.513 17.5911ZM12 14.747L10.5 13.247V10.5C10.5 9.67157 9.82843 9 9 9C8.25144 9 7.63095 9.54832 7.51827 10.2652L6.34845 9.09541C6.85223 8.14635 7.85064 7.5 9 7.5C10.6569 7.5 12 8.84315 12 10.5V14.747ZM13.3623 16.1092L14.5462 17.2932C14.8386 16.5867 15 15.8122 15 15C15 14.5858 14.6642 14.25 14.25 14.25C13.8358 14.25 13.5 14.5858 13.5 15C13.5 15.3828 13.4522 15.7544 13.3623 16.1092Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 4.71869V6C16 6.93329 16.6393 7.71727 17.5038 7.93797C17.5013 7.95829 17.5 7.97899 17.5 8V8.95852C16.0811 8.72048 15 7.4865 15 6C15 5.72386 14.7761 5.5 14.5 5.5C14.2239 5.5 14 5.72386 14 6C14 8.03981 15.5268 9.723 17.5 9.96905V10.5C17.5 10.7761 17.7239 11 18 11C18.2761 11 18.5 10.7761 18.5 10.5V9.96905C19.2275 9.87834 19.8943 9.59227 20.4463 9.16499L22.1324 10.8511C22.3309 11.0496 22.6527 11.0496 22.8512 10.8512C23.0496 10.6527 23.0496 10.3309 22.8511 10.1324L21.165 8.4463C21.1668 8.44393 21.1687 8.44155 21.1705 8.43918L20.4554 7.7241C20.4537 7.72656 20.4519 7.72903 20.4502 7.73149L19.7274 7.00869C19.7289 7.00603 19.7305 7.00338 19.732 7.00072L18.9711 6.23977C18.9702 6.24313 18.9694 6.24649 18.9685 6.24984L17 4.28131V4.26869L16 3.26869V3.28131L13.8676 1.14893C13.6691 0.950402 13.3473 0.950351 13.1488 1.14881C12.9504 1.34727 12.9504 1.6691 13.1489 1.86762L16 4.71869ZM17 5.71869V6C17 6.55228 17.4477 7 18 7C18.0863 7 18.17 6.98908 18.2498 6.96854L17 5.71869ZM19.0087 7.72738C18.8504 7.81999 18.6783 7.89148 18.4962 7.93797C18.4987 7.95829 18.5 7.97899 18.5 8V8.95852C18.953 8.88252 19.3715 8.70502 19.7315 8.45019L19.0087 7.72738ZM20 5.83131L19 4.83131V3C19 2.44772 18.5523 2 18 2C17.501 2 17.0873 2.36555 17.0122 2.84348L16.2323 2.06361C16.5682 1.4309 17.2338 1 18 1C19.1046 1 20 1.89543 20 3V5.83131ZM20.9082 6.73948L21.6975 7.52877C21.8924 7.05778 22 6.54145 22 6C22 5.72386 21.7761 5.5 21.5 5.5C21.2239 5.5 21 5.72386 21 6C21 6.25519 20.9681 6.50294 20.9082 6.73948Z" />

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7916 15.5833H15.4218C15.3159 14.9082 15.1566 14.2976 14.9401 13.75H18.3275C18.331 13.6843 18.3333 13.6079 18.3333 13.5208C18.3333 10.1308 17.531 9.16667 14.6666 9.16667C13.9217 9.16667 13.3162 9.23188 12.828 9.38802C12.8315 9.31453 12.8333 9.24072 12.8333 9.16667C12.8333 7.88484 12.3071 6.72592 11.4589 5.89413C11.4931 4.15185 12.9162 2.75 14.6666 2.75C16.4386 2.75 17.875 4.18642 17.875 5.95833C17.875 6.619 17.6753 7.23302 17.333 7.74334C19.4136 8.53185 20.1666 10.4577 20.1666 13.5208C20.1666 14.8958 19.7083 15.5833 18.7916 15.5833ZM16.0416 5.95833C16.0416 6.71772 15.426 7.33333 14.6666 7.33333C13.9073 7.33333 13.2916 6.71772 13.2916 5.95833C13.2916 5.19894 13.9073 4.58333 14.6666 4.58333C15.426 4.58333 16.0416 5.19894 16.0416 5.95833ZM3.43748 20.1667C2.36804 20.1667 1.83331 19.4028 1.83331 17.875C1.83331 14.3822 2.75854 12.2203 5.33347 11.3892C4.86283 10.7726 4.58331 10.0023 4.58331 9.16667C4.58331 7.14162 6.22494 5.5 8.24998 5.5C10.275 5.5 11.9166 7.14162 11.9166 9.16667C11.9166 10.0023 11.6371 10.7726 11.1665 11.3892C13.7414 12.2203 14.6666 14.3822 14.6666 17.875C14.6666 19.4028 14.1319 20.1667 13.0625 20.1667H3.43748ZM10.0833 9.16667C10.0833 10.1792 9.2625 11 8.24998 11C7.23746 11 6.41665 10.1792 6.41665 9.16667C6.41665 8.15414 7.23746 7.33333 8.24998 7.33333C9.2625 7.33333 10.0833 8.15414 10.0833 9.16667ZM12.8333 17.875C12.8333 18.0711 12.8222 18.2237 12.8084 18.3333H3.69156C3.6778 18.2237 3.66665 18.0711 3.66665 17.875C3.66665 14.0191 4.7028 12.8333 8.24998 12.8333C11.7972 12.8333 12.8333 14.0191 12.8333 17.875Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.74988 2.625V3.95455V9V9.75C9.74988 10.1642 10.0857 10.5 10.4999 10.5C10.9141 10.5 11.2499 10.1642 11.2499 9.75V9V3.95455C11.2499 3.87516 11.3876 3.75 11.6249 3.75C11.8622 3.75 11.9999 3.87516 11.9999 3.95455V5.625V9.75C11.9999 10.1642 12.3357 10.5 12.7499 10.5C13.1641 10.5 13.4999 10.1642 13.4999 9.75V5.625C13.4999 5.41789 13.6678 5.25 13.8749 5.25C14.082 5.25 14.2499 5.41789 14.2499 5.625V11.2811C14.0457 13.3687 12.266 15 10.1249 15C8.71915 15 7.43958 14.2916 6.68469 13.1525L6.682 13.1532L3.82247 8.85337C3.68527 8.65681 3.7265 8.42298 3.89615 8.30418C4.06581 8.18539 4.29963 8.22662 4.41843 8.39627L5.37775 9.83045L5.37678 9.8311C5.60841 10.1745 6.07456 10.2651 6.41796 10.0335C6.62525 9.89367 6.74042 9.66839 6.74821 9.43624L6.74988 9.43673V4.125C6.74988 3.91789 6.91777 3.75 7.12488 3.75C7.33199 3.75 7.49988 3.91789 7.49988 4.125V9V9.75C7.49988 10.1642 7.83567 10.5 8.24988 10.5C8.66409 10.5 8.99988 10.1642 8.99988 9.75V9V4.125V2.625C8.99988 2.41789 9.16777 2.25 9.37488 2.25C9.58199 2.25 9.74988 2.41789 9.74988 2.625ZM15.7366 11.2652L15.7499 11.2586V10.875V5.625C15.7499 4.58947 14.9104 3.75 13.8749 3.75C13.7434 3.75 13.615 3.76354 13.4912 3.78929C13.3998 2.92544 12.5991 2.25 11.6249 2.25C11.4859 2.25 11.3504 2.26375 11.22 2.28984C11.062 1.41423 10.296 0.75 9.37488 0.75C8.4524 0.75 7.68552 1.41617 7.52906 2.29368C7.39889 2.26508 7.26364 2.25 7.12488 2.25C6.08934 2.25 5.24988 3.08947 5.24988 4.125V7.12106C4.61807 6.63808 3.72195 6.595 3.03579 7.07546C2.18753 7.66941 1.98138 8.83856 2.57533 9.68682L5.27627 13.7284C6.25454 15.3871 8.05975 16.5 10.1249 16.5C13.1003 16.5 15.5362 14.1898 15.7366 11.2652Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.21892 4.99996H6.19791L3.11278 1.91484C2.78191 1.58396 2.24554 1.58388 1.91477 1.91465C1.584 2.24542 1.58409 2.78179 1.91496 3.11266L3.80226 4.99996H3.33341C2.41294 4.99996 1.66675 5.74615 1.66675 6.66663V13.3333C1.66675 14.2538 2.41294 15 3.33341 15H12.5001C12.8673 15 13.2068 14.8812 13.4823 14.68L16.8874 18.0851C17.2183 18.416 17.7546 18.416 18.0854 18.0853C18.4162 17.7545 18.4161 17.2181 18.0852 16.8873L14.1667 12.9688V12.9478L12.5001 11.2811V11.3021L7.86457 6.66663H7.88559L6.21892 4.99996ZM12.5001 8.88547V8.33329V6.66663H10.2812L8.61457 4.99996H12.5001C13.4206 4.99996 14.1667 5.74615 14.1667 6.66663V7.38091L17.0866 5.71241C17.4862 5.48407 17.9953 5.6229 18.2236 6.02249C18.2956 6.14841 18.3334 6.29092 18.3334 6.43594V13.564C18.3334 13.8767 18.1612 14.1492 17.9064 14.2917L14.5104 10.8958L16.6667 12.128V7.87193L14.1667 9.3005V10.5521L12.5001 8.88547ZM3.33341 6.66663H5.46892L12.1356 13.3333H3.33341V6.66663Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -3,6 +3,8 @@
import type { Dispatch } from 'redux';
import { CHAT_SIZE } from '../../chat/constants';
import { getParticipantsPaneOpen } from '../../participants-pane/functions';
import theme from '../../participants-pane/theme.json';
import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes';
import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants';
@ -28,11 +30,18 @@ const REDUCED_UI_THRESHOLD = 300;
export function clientResized(clientWidth: number, clientHeight: number) {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { isOpen } = state['features/chat'];
const { isOpen: isChatOpen } = state['features/chat'];
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
let availableWidth = clientWidth;
if (isOpen && navigator.product !== 'ReactNative') {
availableWidth -= CHAT_SIZE;
if (navigator.product !== 'ReactNative') {
if (isChatOpen) {
availableWidth -= CHAT_SIZE;
}
if (isParticipantsPaneOpen) {
availableWidth -= theme.participantsPaneWidth;
}
}
return dispatch({

View File

@ -12,6 +12,78 @@ import {
import loadEffects from './loadEffects';
import logger from './logger';
/**
* Returns root tracks state.
*
* @param {Object} state - Global state.
* @returns {Object} Tracks state.
*/
export const getTrackState = state => state['features/base/tracks'];
/**
* Higher-order function that returns a selector for a specific participant
* and media type.
*
* @param {Object} participant - Participant reference.
* @param {MEDIA_TYPE} mediaType - Media type.
* @returns {Function} Selector.
*/
export const getIsParticipantMediaMuted = (participant, mediaType) =>
/**
* Bound selector.
*
* @param {Object} state - Global state.
* @returns {boolean} Is the media type muted for the participant.
*/
state => {
if (!participant) {
return;
}
const tracks = getTrackState(state);
if (participant?.local) {
return isLocalTrackMuted(tracks, mediaType);
} else if (!participant?.isFakeParticipant) {
return isRemoteTrackMuted(tracks, mediaType, participant.id);
}
return true;
};
/**
* Higher-order function that returns a selector for a specific participant.
*
* @param {Object} participant - Participant reference.
* @returns {Function} Selector.
*/
export const getIsParticipantAudioMuted = participant =>
/**
* Bound selector.
*
* @param {Object} state - Global state.
* @returns {boolean} Is audio muted for the participant.
*/
state => getIsParticipantMediaMuted(participant, MEDIA_TYPE.AUDIO)(state);
/**
* Higher-order function that returns a selector for a specific participant.
*
* @param {Object} participant - Participant reference.
* @returns {Function} Selector.
*/
export const getIsParticipantVideoMuted = participant =>
/**
* Bound selector.
*
* @param {Object} state - Global state.
* @returns {boolean} Is video muted for the participant.
*/
state => getIsParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO)(state);
/**
* Creates a local video track for presenter. The constraints are computed based
* on the height of the desktop that is being shared.
@ -311,7 +383,7 @@ export function getLocalVideoType(tracks) {
* @returns {Object}
*/
export function getLocalJitsiVideoTrack(state) {
const track = getLocalVideoTrack(state['features/base/tracks']);
const track = getLocalVideoTrack(getTrackState(state));
return track?.jitsiTrack;
}
@ -323,7 +395,7 @@ export function getLocalJitsiVideoTrack(state) {
* @returns {Object}
*/
export function getLocalJitsiAudioTrack(state) {
const track = getLocalAudioTrack(state['features/base/tracks']);
const track = getLocalAudioTrack(getTrackState(state));
return track?.jitsiTrack;
}
@ -413,7 +485,7 @@ export function isLocalTrackMuted(tracks, mediaType) {
* @returns {boolean}
*/
export function isLocalVideoTrackDesktop(state) {
const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
const videoTrack = getLocalVideoTrack(getTrackState(state));
return videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
}

View File

@ -14,6 +14,8 @@ import { Filmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
import { ParticipantsPane } from '../../../participants-pane/components';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
import { Toolbox } from '../../../toolbox/components/web';
@ -72,6 +74,11 @@ type Props = AbstractProps & {
*/
_isLobbyScreenVisible: boolean,
/**
* If participants pane is visible or not.
*/
_isParticipantsPaneVisible: boolean,
/**
* The CSS class to apply to the root of {@link Conference} to modify the
* application layout.
@ -179,33 +186,38 @@ class Conference extends AbstractConference<Props, *> {
render() {
const {
_isLobbyScreenVisible,
_isParticipantsPaneVisible,
_layoutClassName,
_showPrejoin
} = this.props;
return (
<div
className = { _layoutClassName }
id = 'videoconference_page'
onMouseMove = { this._onShowToolbar }
ref = { this._setBackground }>
<ConferenceInfo />
<div id = 'layout_wrapper'>
<div
className = { _layoutClassName }
id = 'videoconference_page'
onMouseMove = { this._onShowToolbar }
ref = { this._setBackground }>
<ConferenceInfo />
<Notice />
<div id = 'videospace'>
<LargeVideo />
{!_isParticipantsPaneVisible && <KnockingParticipantList />}
<Filmstrip />
</div>
{ _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
<Chat />
{ this.renderNotificationsContainer() }
<CalleeInfoContainer />
{ _showPrejoin && <Prejoin />}
<Notice />
<div id = 'videospace'>
<LargeVideo />
<KnockingParticipantList />
<Filmstrip />
</div>
{ _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
<Chat />
{ this.renderNotificationsContainer() }
<CalleeInfoContainer />
{ _showPrejoin && <Prejoin />}
<ParticipantsPane />
</div>
);
}
@ -297,6 +309,7 @@ function _mapStateToProps(state) {
...abstractMapStateToProps(state),
_backgroundAlpha: state['features/base/config'].backgroundAlpha,
_isLobbyScreenVisible: state['features/base/dialog']?.component === LobbyScreen,
_isParticipantsPaneVisible: getParticipantsPaneOpen(state),
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
_roomName: getConferenceNameForTitle(state),
_showPrejoin: isPrejoinPageVisible(state)

View File

@ -3,6 +3,7 @@
import { StateListenerRegistry, equals } from '../base/redux';
import { clientResized } from '../base/responsive-ui';
import { setFilmstripVisible } from '../filmstrip/actions';
import { getParticipantsPaneOpen } from '../participants-pane/functions';
import { setOverflowDrawer } from '../toolbox/actions.web';
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
@ -92,6 +93,19 @@ StateListenerRegistry.register(
store.dispatch(clientResized(innerWidth, innerHeight));
});
/**
* Listens for changes in the participant pane state to calculate the
* dimensions of the tile view grid and the tiles.
*/
StateListenerRegistry.register(
/* selector */ getParticipantsPaneOpen,
/* listener */ (isOpen, store) => {
const { innerWidth, innerHeight } = window;
store.dispatch(clientResized(innerWidth, innerHeight));
});
/**
* Listens for changes in the client width to determine whether the overflow menu(s) should be displayed as drawers.
*/

View File

@ -4,6 +4,7 @@ import { PureComponent } from 'react';
import { isLocalParticipantModerator } from '../../base/participants';
import { setKnockingParticipantApproval } from '../actions';
import { getLobbyState } from '../functions';
export type Props = {
@ -66,7 +67,7 @@ export default class AbstractKnockingParticipantList<P: Props = Props> extends P
* @returns {Props}
*/
export function mapStateToProps(state: Object): $Shape<Props> {
const { knockingParticipants, lobbyEnabled } = state['features/lobby'];
const { knockingParticipants, lobbyEnabled } = getLobbyState(state);
return {
_participants: knockingParticipants,

View File

@ -21,3 +21,14 @@ export function setKnockingParticipantApproval(getState: Function, id: string, a
}
}
}
/**
* Selector to return lobby state.
*
* @param {any} state - State object.
* @returns {any}
*/
export function getLobbyState(state: any) {
return state['features/lobby'];
}

View File

@ -0,0 +1,9 @@
/**
* Action type to signal the closing of the participants pane.
*/
export const PARTICIPANTS_PANE_CLOSE = 'PARTICIPANTS_PANE_CLOSE';
/**
* Action type to signal the opening of the participants pane.
*/
export const PARTICIPANTS_PANE_OPEN = 'PARTICIPANTS_PANE_OPEN';

View File

@ -0,0 +1,26 @@
import {
PARTICIPANTS_PANE_CLOSE,
PARTICIPANTS_PANE_OPEN
} from './actionTypes';
/**
* Action to close the participants pane.
*
* @returns {Object}
*/
export const close = () => {
return {
type: PARTICIPANTS_PANE_CLOSE
};
};
/**
* Action to open the participants pane.
*
* @returns {Object}
*/
export const open = () => {
return {
type: PARTICIPANTS_PANE_OPEN
};
};

View File

@ -0,0 +1,32 @@
// @flow
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { Icon, IconInviteMore } from '../../base/icons';
import { beginAddPeople } from '../../invite';
import { ParticipantInviteButton } from './styled';
export const InviteButton = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const onInvite = useCallback(() => {
sendAnalytics(createToolbarEvent('invite'));
dispatch(beginAddPeople());
}, [ dispatch ]);
return (
<ParticipantInviteButton
aria-label = { t('toolbar.accessibilityLabel.invite') }
onClick = { onInvite }>
<Icon
size = { 20 }
src = { IconInviteMore } />
<span>Invite Someone</span>
</ParticipantInviteButton>
);
};

View File

@ -0,0 +1,44 @@
// @flow
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { setKnockingParticipantApproval } from '../../lobby/actions';
import { ActionTrigger, MediaState } from '../constants';
import { ParticipantItem } from './ParticipantItem';
import { ParticipantActionButton } from './styled';
type Props = {
/**
* Participant reference
*/
participant: Object
};
export const LobbyParticipantItem = ({ participant: p }: Props) => {
const dispatch = useDispatch();
const admit = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, true), [ dispatch ]));
const reject = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, false), [ dispatch ]));
const { t } = useTranslation();
return (
<ParticipantItem
actionsTrigger = { ActionTrigger.Permanent }
audioMuteState = { MediaState.None }
participant = { p }
videoMuteState = { MediaState.None }>
<ParticipantActionButton
onClick = { reject }>
{t('lobby.reject')}
</ParticipantActionButton>
<ParticipantActionButton
onClick = { admit }
primary = { true }>
{t('lobby.admit')}
</ParticipantActionButton>
</ParticipantItem>
);
};

View File

@ -0,0 +1,35 @@
// @flow
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { getLobbyState } from '../../lobby/functions';
import { LobbyParticipantItem } from './LobbyParticipantItem';
import { Heading } from './styled';
export const LobbyParticipantList = () => {
const {
lobbyEnabled,
knockingParticipants: participants
} = useSelector(getLobbyState);
const { t } = useTranslation();
if (!lobbyEnabled || !participants.length) {
return null;
}
return (
<>
<Heading>{t('participantsPane.headings.lobby', { count: participants.length })}</Heading>
<div>
{participants.map(p => (
<LobbyParticipantItem
key = { p.id }
participant = { p } />)
)}
</div>
</>
);
};

View File

@ -0,0 +1,166 @@
// @flow
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { openDialog } from '../../base/dialog';
import {
IconCloseCircle,
IconCrown,
IconMessage,
IconMuteEveryoneElse,
IconVideoOff
} from '../../base/icons';
import { isLocalParticipantModerator } from '../../base/participants';
import { getIsParticipantVideoMuted } from '../../base/tracks';
import { openChat } from '../../chat/actions';
import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../video-menu';
import MuteRemoteParticipantsVideoDialog from '../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
import { getComputedOuterHeight } from '../functions';
import {
ContextMenu,
ContextMenuIcon,
ContextMenuItem,
ContextMenuItemGroup,
ignoredChildClassName
} from './styled';
type Props = {
/**
* 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
};
export const MeetingParticipantContextMenu = ({
offsetTarget,
onEnter,
onLeave,
onSelect,
participant
}: Props) => {
const dispatch = useDispatch();
const containerRef = useRef(null);
const isLocalModerator = useSelector(isLocalParticipantModerator);
const isParticipantVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
const [ isHidden, setIsHidden ] = useState(true);
const { t } = useTranslation();
useLayoutEffect(() => {
if (participant
&& containerRef.current
&& offsetTarget?.offsetParent
&& offsetTarget.offsetParent instanceof HTMLElement
) {
const { current: container } = containerRef;
const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
const outerHeight = getComputedOuterHeight(container);
container.style.top = offsetTop + outerHeight > offsetHeight + scrollTop
? offsetTop - outerHeight
: offsetTop;
setIsHidden(false);
} else {
setIsHidden(true);
}
}, [ participant, offsetTarget ]);
const grantModerator = useCallback(() => {
dispatch(openDialog(GrantModeratorDialog, {
participantID: participant.id
}));
}, [ dispatch, participant ]);
const kick = useCallback(() => {
dispatch(openDialog(KickRemoteParticipantDialog, {
participantID: participant.id
}));
}, [ dispatch, participant ]);
const muteEveryoneElse = useCallback(() => {
dispatch(openDialog(MuteEveryoneDialog, {
exclude: [ participant.id ]
}));
}, [ dispatch, participant ]);
const muteVideo = useCallback(() => {
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
participantID: participant.id
}));
}, [ dispatch, participant ]);
const sendPrivateMessage = useCallback(() => {
dispatch(openChat(participant));
}, [ dispatch, participant ]);
if (!participant) {
return null;
}
return (
<ContextMenu
className = { ignoredChildClassName }
innerRef = { containerRef }
isHidden = { isHidden }
onClick = { onSelect }
onMouseEnter = { onEnter }
onMouseLeave = { onLeave }>
<ContextMenuItemGroup>
{isLocalModerator && (
<ContextMenuItem onClick = { muteEveryoneElse }>
<ContextMenuIcon src = { IconMuteEveryoneElse } />
<span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
</ContextMenuItem>
)}
{isLocalModerator && (isParticipantVideoMuted || (
<ContextMenuItem onClick = { muteVideo }>
<ContextMenuIcon src = { IconVideoOff } />
<span>{t('participantsPane.actions.stopVideo')}</span>
</ContextMenuItem>
))}
</ContextMenuItemGroup>
<ContextMenuItemGroup>
{isLocalModerator && (
<ContextMenuItem onClick = { grantModerator }>
<ContextMenuIcon src = { IconCrown } />
<span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
</ContextMenuItem>
)}
{isLocalModerator && (
<ContextMenuItem onClick = { kick }>
<ContextMenuIcon src = { IconCloseCircle } />
<span>{t('videothumbnail.kick')}</span>
</ContextMenuItem>
)}
<ContextMenuItem onClick = { sendPrivateMessage }>
<ContextMenuIcon src = { IconMessage } />
<span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
</ContextMenuItem>
</ContextMenuItemGroup>
</ContextMenu>
);
};

View File

@ -0,0 +1,55 @@
// @flow
import React from 'react';
import { useSelector } from 'react-redux';
import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
import { ActionTrigger, MediaState } from '../constants';
import { ParticipantItem } from './ParticipantItem';
import { ParticipantActionEllipsis } from './styled';
type Props = {
/**
* Is this item highlighted
*/
isHighlighted: boolean,
/**
* Callback for the activation of this item's context menu
*/
onContextMenu: Function,
/**
* Callback for the mouse leaving this item
*/
onLeave: Function,
/**
* Participant reference
*/
participant: Object
};
export const MeetingParticipantItem = ({
isHighlighted,
onContextMenu,
onLeave,
participant
}: Props) => {
const isAudioMuted = useSelector(getIsParticipantAudioMuted(participant));
const isVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
return (
<ParticipantItem
actionsTrigger = { ActionTrigger.Hover }
audioMuteState = { isAudioMuted ? MediaState.Muted : MediaState.Unmuted }
isHighlighted = { isHighlighted }
onLeave = { onLeave }
participant = { participant }
videoMuteState = { isVideoMuted ? MediaState.Muted : MediaState.Unmuted }>
<ParticipantActionEllipsis onClick = { onContextMenu } />
</ParticipantItem>
);
};

View File

@ -0,0 +1,108 @@
// @flow
import _ from 'lodash';
import React, { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { getParticipants } from '../../base/participants';
import { findStyledAncestor } from '../functions';
import { InviteButton } from './InviteButton';
import { MeetingParticipantContextMenu } from './MeetingParticipantContextMenu';
import { MeetingParticipantItem } from './MeetingParticipantItem';
import { Heading, ParticipantContainer } from './styled';
type NullProto = {
[key: string]: any,
__proto__: null
};
type RaiseContext = NullProto | {
/**
* Target elements against which positioning calculations are made
*/
offsetTarget?: HTMLElement,
/**
* Participant reference
*/
participant?: Object,
};
const initialState = Object.freeze(Object.create(null));
export const MeetingParticipantList = () => {
const isMouseOverMenu = useRef(false);
const participants = useSelector(getParticipants, _.isEqual);
const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
const { t } = useTranslation();
const lowerMenu = useCallback(() => {
/**
* We are tracking mouse movement over the active participant item and
* the context menu. Due to the order of enter/leave events, we need to
* defer checking if the mouse is over the context menu with
* queueMicrotask
*/
window.queueMicrotask(() => {
if (isMouseOverMenu.current) {
return;
}
if (raiseContext !== initialState) {
setRaiseContext(initialState);
}
});
}, [ raiseContext ]);
const raiseMenu = useCallback((participant, target) => {
setRaiseContext({
participant,
offsetTarget: findStyledAncestor(target, ParticipantContainer)
});
}, [ raiseContext ]);
const toggleMenu = useCallback(participant => e => {
const { participant: raisedParticipant } = raiseContext;
if (raisedParticipant && raisedParticipant === participant) {
lowerMenu();
} else {
raiseMenu(participant, e.target);
}
}, [ raiseContext ]);
const menuEnter = useCallback(() => {
isMouseOverMenu.current = true;
}, []);
const menuLeave = useCallback(() => {
isMouseOverMenu.current = false;
lowerMenu();
}, [ lowerMenu ]);
return (
<>
<Heading>{t('participantsPane.headings.participantsList', { count: participants.length })}</Heading>
<InviteButton />
<div>
{participants.map(p => (
<MeetingParticipantItem
isHighlighted = { raiseContext.participant === p }
key = { p.id }
onContextMenu = { toggleMenu(p) }
onLeave = { lowerMenu }
participant = { p } />
))}
</div>
<MeetingParticipantContextMenu
onEnter = { menuEnter }
onLeave = { menuLeave }
onSelect = { lowerMenu }
{ ...raiseContext } />
</>
);
};

View File

@ -0,0 +1,154 @@
// @flow
import React, { type Node } from 'react';
import { useTranslation } from 'react-i18next';
import { Avatar } from '../../base/avatar';
import {
Icon,
IconCameraEmpty,
IconCameraEmptyDisabled,
IconMicrophoneEmpty,
IconMicrophoneEmptySlash
} from '../../base/icons';
import { ActionTrigger, MediaState } from '../constants';
import { RaisedHandIndicator } from './RaisedHandIndicator';
import {
ParticipantActionsHover,
ParticipantActionsPermanent,
ParticipantContainer,
ParticipantContent,
ParticipantName,
ParticipantNameContainer,
ParticipantStates
} from './styled';
/**
* Participant actions component mapping depending on trigger type.
*/
const Actions = {
[ActionTrigger.Hover]: ParticipantActionsHover,
[ActionTrigger.Permanent]: ParticipantActionsPermanent
};
/**
* Icon mapping for possible participant audio states.
*/
const AudioStateIcons = {
[MediaState.ForceMuted]: (
<Icon
size = { 16 }
src = { IconMicrophoneEmptySlash } />
),
[MediaState.Muted]: (
<Icon
size = { 16 }
src = { IconMicrophoneEmptySlash } />
),
[MediaState.Unmuted]: (
<Icon
size = { 16 }
src = { IconMicrophoneEmpty } />
),
[MediaState.None]: null
};
/**
* Icon mapping for possible participant video states.
*/
const VideoStateIcons = {
[MediaState.ForceMuted]: (
<Icon
size = { 16 }
src = { IconCameraEmptyDisabled } />
),
[MediaState.Muted]: (
<Icon
size = { 16 }
src = { IconCameraEmptyDisabled } />
),
[MediaState.Unmuted]: (
<Icon
size = { 16 }
src = { IconCameraEmpty } />
),
[MediaState.None]: null
};
type Props = {
/**
* Type of trigger for the participant actions
*/
actionsTrigger: ActionTrigger,
/**
* Media state for audio
*/
audioMuteState: MediaState,
/**
* React children
*/
children: Node,
/**
* Is this item highlighted/raised
*/
isHighlighted?: boolean,
/**
* Callback for when the mouse leaves this component
*/
onLeave?: Function,
/**
* Participant reference
*/
participant: Object,
/**
* Media state for video
*/
videoMuteState: MediaState
}
export const ParticipantItem = ({
children,
isHighlighted,
onLeave,
actionsTrigger = ActionTrigger.Hover,
audioMuteState = MediaState.None,
videoMuteState = MediaState.None,
participant: p
}: Props) => {
const ParticipantActions = Actions[actionsTrigger];
const { t } = useTranslation();
return (
<ParticipantContainer
isHighlighted = { isHighlighted }
onMouseLeave = { onLeave }
trigger = { actionsTrigger }>
<Avatar
className = 'participant-avatar'
participantId = { p.id }
size = { 32 } />
<ParticipantContent>
<ParticipantNameContainer>
<ParticipantName>
{ p.name }
</ParticipantName>
{ p.local ? <span>&nbsp;({t('chat.you')})</span> : null }
</ParticipantNameContainer>
{ !p.local && <ParticipantActions children = { children } /> }
<ParticipantStates>
{p.raisedHand && <RaisedHandIndicator />}
{VideoStateIcons[videoMuteState]}
{AudioStateIcons[audioMuteState]}
</ParticipantStates>
</ParticipantContent>
</ParticipantContainer>
);
};

View File

@ -0,0 +1,62 @@
// @flow
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import { openDialog } from '../../base/dialog';
import { isLocalParticipantModerator } from '../../base/participants';
import { MuteEveryoneDialog } from '../../video-menu/components/';
import { close } from '../actions';
import { classList, getParticipantsPaneOpen } from '../functions';
import theme from '../theme.json';
import { LobbyParticipantList } from './LobbyParticipantList';
import { MeetingParticipantList } from './MeetingParticipantList';
import {
AntiCollapse,
Close,
Container,
Footer,
FooterButton,
Header
} from './styled';
export const ParticipantsPane = () => {
const dispatch = useDispatch();
const paneOpen = useSelector(getParticipantsPaneOpen);
const isLocalModerator = useSelector(isLocalParticipantModerator);
const { t } = useTranslation();
const closePane = useCallback(() => dispatch(close(), [ dispatch ]));
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]);
return (
<ThemeProvider theme = { theme }>
<div
className = { classList(
'participants_pane',
!paneOpen && 'participants_pane--closed'
) }>
<div className = 'participants_pane-content'>
<Header>
<Close onClick = { closePane } />
</Header>
<Container>
<LobbyParticipantList />
<AntiCollapse />
<MeetingParticipantList />
</Container>
{isLocalModerator && (
<Footer>
<FooterButton onClick = { muteAll }>
{t('participantsPane.actions.muteAll')}
</FooterButton>
</Footer>
)}
</div>
</div>
</ThemeProvider>
);
};

View File

@ -0,0 +1,15 @@
// @flow
import React from 'react';
import { Icon, IconRaisedHandHollow } from '../../base/icons';
import { RaisedHandIndicatorBackground } from './styled';
export const RaisedHandIndicator = () => (
<RaisedHandIndicatorBackground>
<Icon
size = { 15 }
src = { IconRaisedHandHollow } />
</RaisedHandIndicatorBackground>
);

View File

@ -0,0 +1,9 @@
export * from './InviteButton';
export * from './LobbyParticipantItem';
export * from './LobbyParticipantList';
export * from './MeetingParticipantContextMenu';
export * from './MeetingParticipantItem';
export * from './MeetingParticipantList';
export * from './ParticipantItem';
export * from './ParticipantsPane';
export * from './RaisedHandIndicator';

View File

@ -0,0 +1,335 @@
import React from 'react';
import styled from 'styled-components';
import { Icon, IconHorizontalPoints } from '../../base/icons';
import { ActionTrigger } from '../constants';
export const ignoredChildClassName = 'ignore-child';
export const AntiCollapse = styled.br`
font-size: 0;
`;
export const Button = styled.button`
align-items: center;
background-color: ${
// eslint-disable-next-line no-confusing-arrow
props => props.primary ? '#0056E0' : '#3D3D3D'
};
border: 0;
border-radius: 6px;
display: flex;
font-weight: unset;
justify-content: center;
&:hover {
background-color: ${
// eslint-disable-next-line no-confusing-arrow
props => props.primary ? '#246FE5' : '#525252'
};
}
`;
export const Container = styled.div`
box-sizing: border-box;
flex: 1;
overflow-y: auto;
position: relative;
padding: 0 ${props => props.theme.panePadding}px;
& > * + *:not(.${ignoredChildClassName}) {
margin-top: 16px;
}
&::-webkit-scrollbar {
display: none;
}
`;
export const ContextMenu = styled.div.attrs(props => {
return {
className: props.className
};
})`
background-color: #292929;
border-radius: 3px;
box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
color: white;
font-size: ${props => props.theme.contextFontSize}px;
font-weight: ${props => props.theme.contextFontWeight};
margin-top: ${props => {
const {
participantActionButtonHeight,
participantItemHeight
} = props.theme;
return ((3 * participantItemHeight) + participantActionButtonHeight) / 4;
}}px;
position: absolute;
right: ${props => props.theme.panePadding}px;
top: 0;
z-index: 2;
& > li {
list-style: none;
}
${props => props.isHidden && `
pointer-events: none;
visibility: hidden;
`}
`;
export const ContextMenuIcon = styled(Icon).attrs({
size: 20
})`
& > svg {
fill: #a4b8d1;
}
`;
export const ContextMenuItem = styled.div`
align-items: center;
box-sizing: border-box;
cursor: pointer;
display: flex;
height: 40px;
padding: 8px 16px;
& > *:not(:last-child) {
margin-right: 16px;
}
&:hover {
background-color: #525252;
}
`;
export const ContextMenuItemGroup = styled.div`
&:not(:empty) {
padding: 8px 0;
}
& + &:not(:empty) {
border-top: 1px solid #4C4D50;
}
`;
export const Close = styled.div`
align-items: center;
cursor: pointer;
display: flex;
height: 20px;
justify-content: center;
width: 20px;
&:before, &:after {
content: '';
background-color: #a4b8d1;
border-radius: 2px;
height: 2px;
position: absolute;
transform-origin: center center;
width: 21px;
}
&:before {
transform: rotate(45deg);
}
&:after {
transform: rotate(-45deg);
}
`;
export const Footer = styled.div`
background-color: #141414;
display: flex;
justify-content: flex-end;
padding: 24px ${props => props.theme.panePadding}px;
& > *:not(:last-child) {
margin-right: 16px;
}
`;
export const FooterButton = styled(Button)`
height: 40px;
font-size: 15px;
padding: 0 16px;
`;
export const FooterEllipsisButton = styled(FooterButton).attrs({
children: <Icon src = { IconHorizontalPoints } />
})`
padding: 8px;
`;
export const FooterEllipsisContainer = styled.div`
position: relative;
`;
export const Header = styled.div`
align-items: center;
box-sizing: border-box;
display: flex;
height: ${props => props.theme.headerSize}px;
padding: 0 20px;
`;
export const Heading = styled.div`
color: #d1dbe8;
font-style: normal;
font-size: 15px;
line-height: 24px;
margin: 8px 0 ${props => props.theme.panePadding}px;
`;
export const ParticipantActionButton = styled(Button)`
height: ${props => props.theme.participantActionButtonHeight}px;
padding: 6px 10px;
`;
export const ParticipantActionEllipsis = styled(ParticipantActionButton).attrs({
children: <Icon src = { IconHorizontalPoints } />,
primary: true
})`
padding: 6px;
`;
export const ParticipantActions = styled.div`
align-items: center;
z-index: 1;
& > *:not(:last-child) {
margin-right: 8px;
}
`;
export const ParticipantActionsHover = styled(ParticipantActions)`
background-color: #292929;
bottom: 1px;
display: none;
position: absolute;
right: ${props => props.theme.panePadding};
top: 0;
&:after {
content: '';
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #292929 100%);
bottom: 0;
display: block;
left: 0;
pointer-events: none;
position: absolute;
top: 0;
transform: translateX(-100%);
width: 40px;
}
`;
export const ParticipantActionsPermanent = styled(ParticipantActions)`
display: flex;
`;
export const ParticipantContent = styled.div`
align-items: center;
box-shadow: inset 0px -1px 0px rgba(255, 255, 255, 0.15);
display: flex;
flex: 1;
height: 100%;
overflow: hidden;
padding-right: ${props => props.theme.panePadding}px;
`;
export const ParticipantContainer = styled.div`
align-items: center;
color: white;
display: flex;
font-size: 13px;
height: ${props => props.theme.participantItemHeight}px;
margin: 0 -${props => props.theme.panePadding}px;
padding-left: ${props => props.theme.panePadding}px;
position: relative;
${props => !props.isHighlighted && '&:hover {'}
background-color: #292929;
& ${ParticipantActions} {
${props => props.trigger === ActionTrigger.Hover && `
display: flex;
`}
}
& ${ParticipantContent} {
box-shadow: none;
}
${props => !props.isHighlighted && '}'}
`;
export const ParticipantInviteButton = styled(Button).attrs({
primary: true
})`
font-size: 15px;
height: 40px;
width: 100%;
& > *:not(:last-child) {
margin-right: 8px;
}
`;
export const ParticipantName = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const ParticipantNameContainer = styled.div`
display: flex;
flex: 1;
margin-right: 8px;
overflow: hidden;
`;
export const ParticipantStates = styled.div`
display: flex;
justify-content: flex-end;
& > * {
align-items: center;
display: flex;
justify-content: center;
}
& > *:not(:last-child) {
margin-right: 8px;
}
`;
export const RaisedHandIndicatorBackground = styled.div`
background-color: #ed9e1b;
border-radius: 3px;
height: 24px;
width: 24px;
`;
export const VolumeInput = styled.input.attrs({
type: 'range'
})`
width: 100%;
`;
export const VolumeInputContainer = styled.div`
position: relative;
width: 100%;
`;
export const VolumeOverlay = styled.div`
background-color: #0376da;
border-radius: 1px 0 0 1px;
height: 100%;
left: 0;
pointer-events: none;
position: absolute;
`;

View File

@ -0,0 +1,22 @@
/**
* Reducer key for the feature.
*/
export const REDUCER_KEY = 'features/participants-pane';
/**
* Enum of possible participant action triggers.
*/
export const ActionTrigger = {
Hover: 'ActionTrigger.Hover',
Permanent: 'ActionTrigger.Permanent'
};
/**
* Enum of possible participant media states.
*/
export const MediaState = {
Muted: 'MediaState.Muted',
ForceMuted: 'MediaState.ForceMuted',
Unmuted: 'MediaState.Unmuted',
None: 'MediaState.None'
};

View File

@ -0,0 +1,66 @@
import { REDUCER_KEY } from './constants';
/**
* Generates a class attribute value.
*
* @param {Iterable<string>} args - String iterable.
* @returns {string} Class attribute value.
*/
export const classList = (...args) => args.filter(Boolean).join(' ');
/**
* Find the first styled ancestor component of an element.
*
* @param {Element} target - Element to look up.
* @param {StyledComponentClass} component - Styled component reference.
* @returns {Element|null} Ancestor.
*/
export const findStyledAncestor = (target, component) => {
if (!target || target.matches(`.${component.styledComponentId}`)) {
return target;
}
return findStyledAncestor(target.parentElement, component);
};
/**
* Get a style property from a style declaration as a float.
*
* @param {CSSStyleDeclaration} styles - Style declaration.
* @param {string} name - Property name.
* @returns {number} Float value.
*/
export const getFloatStyleProperty = (styles, name) =>
parseFloat(styles.getPropertyValue(name));
/**
* Gets the outer height of an element, including margins.
*
* @param {Element} element - Target element.
* @returns {number} Computed height.
*/
export const getComputedOuterHeight = element => {
const computedStyle = getComputedStyle(element);
return element.offsetHeight
+ getFloatStyleProperty(computedStyle, 'margin-top')
+ getFloatStyleProperty(computedStyle, 'margin-bottom');
};
/**
* Returns this feature's root state.
*
* @param {Object} state - Global state.
* @returns {Object} Feature state.
*/
const getState = state => state[REDUCER_KEY];
/**
* Is the participants pane open.
*
* @param {Object} state - Global state.
* @returns {boolean} Is the participants pane open.
*/
export const getParticipantsPaneOpen = state => Boolean(getState(state).isOpen);

View File

@ -0,0 +1,35 @@
import { ReducerRegistry } from '../base/redux';
import {
PARTICIPANTS_PANE_CLOSE,
PARTICIPANTS_PANE_OPEN
} from './actionTypes';
import { REDUCER_KEY } from './constants';
const DEFAULT_STATE = {
isOpen: false
};
/**
* Listen for actions that mutate the participants pane state
*/
ReducerRegistry.register(
REDUCER_KEY, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case PARTICIPANTS_PANE_CLOSE:
return {
...state,
isOpen: false
};
case PARTICIPANTS_PANE_OPEN:
return {
...state,
isOpen: true
};
default:
return state;
}
},
);

View File

@ -0,0 +1,10 @@
{
"contextFontSize": 14,
"contextFontWeight": 400,
"headerSize": 60,
"panePadding": 16,
"participantActionButtonHeight": 32,
"participantItemHeight": 48,
"participantsPaneWidth": 315,
"rangeInputThumbSize": 14
}

View File

@ -3,7 +3,7 @@
import React, { Component } from 'react';
import { translate } from '../../../../base/i18n';
import { IconMicrophoneEmpty, IconVolumeEmpty } from '../../../../base/icons';
import { IconMicrophoneHollow, IconVolumeEmpty } from '../../../../base/icons';
import JitsiMeetJS from '../../../../base/lib-jitsi-meet';
import { equals } from '../../../../base/redux';
import { createLocalAudioTracks } from '../../../functions';
@ -248,7 +248,7 @@ class AudioSettingsContent extends Component<Props, State> {
<div>
<div className = 'audio-preview-content'>
<AudioSettingsHeader
IconComponent = { IconMicrophoneEmpty }
IconComponent = { IconMicrophoneHollow }
text = { t('settings.microphones') } />
{this.state.audioTracks.map((data, i) =>
this._renderMicrophoneEntry(data, i),

View File

@ -19,7 +19,7 @@ import {
IconExitFullScreen,
IconFeedback,
IconFullScreen,
IconInviteMore,
IconParticipants,
IconPresentation,
IconRaisedHand,
IconRec,
@ -37,13 +37,16 @@ import { OverflowMenuItem } from '../../../base/toolbox/components';
import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
import { isVpaasMeeting } from '../../../billing-counter/functions';
import { ChatCounter, toggleChat } from '../../../chat';
import { InviteMore } from '../../../conference';
import { EmbedMeetingDialog } from '../../../embed-meeting';
import { SharedDocumentButton } from '../../../etherpad';
import { openFeedbackDialog } from '../../../feedback';
import { beginAddPeople } from '../../../invite';
import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
import { LocalRecordingInfoDialog } from '../../../local-recording';
import {
close as closeParticipantsPane,
open as openParticipantsPane
} from '../../../participants-pane/actions';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import {
LiveStreamButton,
RecordButton
@ -179,6 +182,11 @@ type Props = {
*/
_overflowMenuVisible: boolean,
/**
* Whether or not the participants pane is open.
*/
_participantsPaneOpen: boolean,
/**
* Whether or not the local participant's hand is raised.
*/
@ -240,11 +248,12 @@ class Toolbox extends Component<Props> {
this._onShortcutToggleChat = this._onShortcutToggleChat.bind(this);
this._onShortcutToggleFullScreen = this._onShortcutToggleFullScreen.bind(this);
this._onShortcutToggleParticipantsPane = this._onShortcutToggleParticipantsPane.bind(this);
this._onShortcutToggleRaiseHand = this._onShortcutToggleRaiseHand.bind(this);
this._onShortcutToggleScreenshare = this._onShortcutToggleScreenshare.bind(this);
this._onShortcutToggleVideoQuality = this._onShortcutToggleVideoQuality.bind(this);
this._onToolbarOpenFeedback = this._onToolbarOpenFeedback.bind(this);
this._onToolbarOpenInvite = this._onToolbarOpenInvite.bind(this);
this._onToolbarToggleParticipantsPane = this._onToolbarToggleParticipantsPane.bind(this);
this._onToolbarOpenKeyboardShortcuts = this._onToolbarOpenKeyboardShortcuts.bind(this);
this._onToolbarOpenSpeakerStats = this._onToolbarOpenSpeakerStats.bind(this);
this._onToolbarOpenEmbedMeeting = this._onToolbarOpenEmbedMeeting.bind(this);
@ -282,6 +291,11 @@ class Toolbox extends Component<Props> {
exec: this._onShortcutToggleScreenshare,
helpDescription: 'keyboardShortcuts.toggleScreensharing'
},
this._shouldShowButton('participants-pane') && {
character: 'P',
exec: this._onShortcutToggleParticipantsPane,
helpDescription: 'keyboardShortcuts.toggleParticipantsPane'
},
this._shouldShowButton('raisehand') && {
character: 'R',
exec: this._onShortcutToggleRaiseHand,
@ -577,6 +591,25 @@ class Toolbox extends Component<Props> {
this._doToggleChat();
}
_onShortcutToggleParticipantsPane: () => void;
/**
* Creates an analytics keyboard shortcut event and dispatches an action for
* toggling the display of the participants pane.
*
* @private
* @returns {void}
*/
_onShortcutToggleParticipantsPane() {
sendAnalytics(createShortcutEvent(
'toggle.participants-pane',
{
enable: !this.props._participantsPaneOpen
}));
this._onToolbarToggleParticipantsPane();
}
_onShortcutToggleVideoQuality: () => void;
/**
@ -694,18 +727,22 @@ class Toolbox extends Component<Props> {
this._doOpenFeedback();
}
_onToolbarOpenInvite: () => void;
_onToolbarToggleParticipantsPane: () => void;
/**
* Creates an analytics toolbar event and dispatches an action for opening
* the modal for inviting people directly into the conference.
* Dispatches an action for toggling the participants pane.
*
* @private
* @returns {void}
*/
_onToolbarOpenInvite() {
sendAnalytics(createToolbarEvent('invite'));
this.props.dispatch(beginAddPeople());
_onToolbarToggleParticipantsPane() {
const { dispatch, _participantsPaneOpen } = this.props;
if (_participantsPaneOpen) {
dispatch(closeParticipantsPane());
} else {
dispatch(openParticipantsPane());
}
}
_onToolbarOpenKeyboardShortcuts: () => void;
@ -1163,6 +1200,25 @@ class Toolbox extends Component<Props> {
text = { t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`) } />);
}
if (this._shouldShowButton('participants-pane') || this._shouldShowButton('invite')) {
buttons.has('participants-pane')
? mainMenuAdditionalButtons.push(
<ToolbarButton
accessibilityLabel = { t('toolbar.accessibilityLabel.participants') }
icon = { IconParticipants }
onClick = { this._onToolbarToggleParticipantsPane }
toggled = { this.props._participantsPaneOpen }
tooltip = { t('toolbar.participants') } />)
: overflowMenuAdditionalButtons.push(
<OverflowMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.participants') }
icon = { IconParticipants }
key = 'participants-pane'
onClick = { this._onToolbarToggleParticipantsPane }
text = { t('toolbar.participants') } />
);
}
if (this._shouldShowButton('tileview')) {
buttons.has('tileview')
? mainMenuAdditionalButtons.push(
@ -1175,25 +1231,6 @@ class Toolbox extends Component<Props> {
showLabel = { true } />);
}
if (this._shouldShowButton('invite')) {
buttons.has('invite')
? mainMenuAdditionalButtons.push(
<ToolbarButton
accessibilityLabel = { t('toolbar.accessibilityLabel.invite') }
icon = { IconInviteMore }
key = 'invite'
onClick = { this._onToolbarOpenInvite }
tooltip = { t('toolbar.invite') } />)
: overflowMenuAdditionalButtons.push(
<OverflowMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.invite') }
icon = { IconInviteMore }
key = 'invite'
onClick = { this._onToolbarOpenInvite }
text = { t('toolbar.invite') } />
);
}
return {
mainMenuAdditionalButtons,
overflowMenuAdditionalButtons
@ -1254,7 +1291,6 @@ class Toolbox extends Component<Props> {
return (
<div className = { containerClassName }>
<div className = 'toolbox-content-wrapper'>
<InviteMore />
<div className = 'toolbox-content-items'>
{ this._renderAudioButton() }
{ this._renderVideoButton() }
@ -1344,6 +1380,7 @@ function _mapStateToProps(state) {
_localRecState: localRecordingStates,
_locked: locked,
_overflowMenuVisible: overflowMenuVisible,
_participantsPaneOpen: getParticipantsPaneOpen(state),
_raisedHand: localParticipant.raisedHand,
_screensharing: (localVideo && localVideo.videoType === 'desktop') || isScreenAudioShared(state),
_visible: isToolboxVisible(state),

View File

@ -25,23 +25,23 @@ export function getToolbarAdditionalButtons(width: number, isMobile: boolean): S
switch (true) {
case width >= WIDTH.FIT_9_ICONS: {
buttons = isMobile
? [ 'chat', 'raisehand', 'tileview', 'invite', 'overflow' ]
: [ 'desktop', 'chat', 'raisehand', 'tileview', 'invite', 'overflow' ];
? [ 'chat', 'raisehand', 'tileview', 'participants-pane', 'overflow' ]
: [ 'desktop', 'chat', 'raisehand', 'tileview', 'participants-pane', 'overflow' ];
break;
}
case width >= WIDTH.FIT_8_ICONS: {
buttons = [ 'desktop', 'chat', 'raisehand', 'invite', 'overflow' ];
buttons = [ 'desktop', 'chat', 'raisehand', 'participants-pane', 'overflow' ];
break;
}
case width >= WIDTH.FIT_7_ICONS: {
buttons = [ 'desktop', 'chat', 'invite', 'overflow' ];
buttons = [ 'desktop', 'chat', 'participants-pane', 'overflow' ];
break;
}
case width >= WIDTH.FIT_6_ICONS: {
buttons = [ 'chat', 'invite', 'overflow' ];
buttons = [ 'chat', 'participants-pane', 'overflow' ];
break;
}

View File

@ -84,7 +84,7 @@ export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRe
* @returns {Props}
*/
export function abstractMapStateToProps(state: Object, ownProps: Props) {
const { exclude, t } = ownProps;
const { exclude = [], t } = ownProps;
const whom = exclude
// eslint-disable-next-line no-confusing-arrow