feat(participants-pane) implement participants pane
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
$rangeInputThumbSize: 14;
|
||||
|
||||
/**
|
||||
* Disable the default webkit styles for range inputs (sliders).
|
||||
*/
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,5 +104,6 @@ $flagsImagePath: "../images/";
|
|||
@import 'responsive';
|
||||
@import 'connection-status';
|
||||
@import 'drawer';
|
||||
@import 'participants-pane';
|
||||
|
||||
/* Modules END */
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
/**
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'
|
||||
];
|
||||
|
|
|
@ -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 |
|
@ -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';
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'];
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -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
|
||||
};
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 } />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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> ({t('chat.you')})</span> : null }
|
||||
</ParticipantNameContainer>
|
||||
{ !p.local && <ParticipantActions children = { children } /> }
|
||||
<ParticipantStates>
|
||||
{p.raisedHand && <RaisedHandIndicator />}
|
||||
{VideoStateIcons[videoMuteState]}
|
||||
{AudioStateIcons[audioMuteState]}
|
||||
</ParticipantStates>
|
||||
</ParticipantContent>
|
||||
</ParticipantContainer>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -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';
|
|
@ -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;
|
||||
`;
|
|
@ -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'
|
||||
};
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
);
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"contextFontSize": 14,
|
||||
"contextFontWeight": 400,
|
||||
"headerSize": 60,
|
||||
"panePadding": 16,
|
||||
"participantActionButtonHeight": 32,
|
||||
"participantItemHeight": 48,
|
||||
"participantsPaneWidth": 315,
|
||||
"rangeInputThumbSize": 14
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|