feat(new-toolbars): initial implementation
This commit is contained in:
parent
962df14382
commit
d93782af8a
|
@ -43,7 +43,8 @@ import {
|
|||
lockStateChanged,
|
||||
onStartMutedPolicyChanged,
|
||||
p2pStatusChanged,
|
||||
sendLocalParticipant
|
||||
sendLocalParticipant,
|
||||
setDesktopSharingEnabled
|
||||
} from './react/features/base/conference';
|
||||
import { updateDeviceList } from './react/features/base/devices';
|
||||
import {
|
||||
|
@ -104,6 +105,7 @@ import {
|
|||
mediaPermissionPromptVisibilityChanged,
|
||||
suspendDetected
|
||||
} from './react/features/overlay';
|
||||
import { setSharedVideoStatus } from './react/features/shared-video';
|
||||
import {
|
||||
isButtonEnabled,
|
||||
showDesktopSharingButton
|
||||
|
@ -505,16 +507,6 @@ export default {
|
|||
*/
|
||||
desktopSharingDisabledTooltip: null,
|
||||
|
||||
/*
|
||||
* Whether the local "raisedHand" flag is on.
|
||||
*/
|
||||
isHandRaised: false,
|
||||
|
||||
/*
|
||||
* Whether the local participant is the dominant speaker in the conference.
|
||||
*/
|
||||
isDominantSpeaker: false,
|
||||
|
||||
/**
|
||||
* The local audio track (if any).
|
||||
* FIXME tracks from redux store should be the single source of truth
|
||||
|
@ -773,6 +765,8 @@ export default {
|
|||
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
|
||||
this.isDesktopSharingEnabled);
|
||||
|
||||
APP.store.dispatch(
|
||||
setDesktopSharingEnabled(this.isDesktopSharingEnabled));
|
||||
APP.store.dispatch(showDesktopSharingButton());
|
||||
|
||||
this._createRoom(tracks);
|
||||
|
@ -1896,19 +1890,6 @@ export default {
|
|||
});
|
||||
room.on(JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, id => {
|
||||
APP.store.dispatch(dominantSpeakerChanged(id));
|
||||
|
||||
if (this.isLocalId(id)) {
|
||||
this.isDominantSpeaker = true;
|
||||
this.setRaisedHand(false);
|
||||
} else {
|
||||
this.isDominantSpeaker = false;
|
||||
const participant = room.getParticipantById(id);
|
||||
|
||||
if (participant) {
|
||||
APP.UI.setRaisedHandStatus(participant, false);
|
||||
}
|
||||
}
|
||||
APP.UI.markDominantSpeaker(id);
|
||||
});
|
||||
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
|
@ -2022,7 +2003,10 @@ export default {
|
|||
(participant, name, oldValue, newValue) => {
|
||||
switch (name) {
|
||||
case 'raisedHand':
|
||||
APP.UI.setRaisedHandStatus(participant, newValue);
|
||||
APP.store.dispatch(participantUpdated({
|
||||
id: participant.getId(),
|
||||
raisedHand: newValue === 'true'
|
||||
}));
|
||||
break;
|
||||
case 'remoteControlSessionStatus':
|
||||
APP.UI.setRemoteControlActiveStatus(
|
||||
|
@ -2361,6 +2345,8 @@ export default {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
APP.store.dispatch(setSharedVideoStatus(state));
|
||||
});
|
||||
room.addCommandListener(
|
||||
this.commands.defaults.SHARED_VIDEO,
|
||||
|
@ -2623,30 +2609,6 @@ export default {
|
|||
APP.API.notifyVideoAvailabilityChanged(available);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the local "raised hand" status.
|
||||
*/
|
||||
maybeToggleRaisedHand() {
|
||||
this.setRaisedHand(!this.isHandRaised);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the local "raised hand" status to a particular value.
|
||||
*/
|
||||
setRaisedHand(raisedHand) {
|
||||
if (raisedHand !== this.isHandRaised) {
|
||||
APP.UI.onLocalRaiseHandChanged(raisedHand);
|
||||
|
||||
this.isHandRaised = raisedHand;
|
||||
|
||||
// Advertise the updated status
|
||||
room.setLocalParticipantProperty('raisedHand', raisedHand);
|
||||
|
||||
// Update the view
|
||||
APP.UI.setLocalRaisedHandStatus(raisedHand);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Disconnect from the conference and optionally request user feedback.
|
||||
* @param {boolean} [requestFeedback=false] if user feedback should be
|
||||
|
|
|
@ -5,6 +5,20 @@
|
|||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.use-new-toolbox {
|
||||
.filmstrip.reduce-height {
|
||||
bottom: $newToolbarSize;
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
transition: bottom .3s;
|
||||
}
|
||||
|
||||
.filmstrip__videos.hidden {
|
||||
bottom: calc(-196px - #{$newToolbarSize});
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
|
|
@ -180,3 +180,6 @@
|
|||
.icon-gsm-bars:before {
|
||||
content: "\e926";
|
||||
}
|
||||
.icon-open_in_new:before {
|
||||
content: "\e89e";
|
||||
}
|
||||
|
|
|
@ -1,6 +1,37 @@
|
|||
/**
|
||||
* Toolbar side panel main container element.
|
||||
*/
|
||||
.use-new-toolbox #sideToolbarContainer {
|
||||
background-color: rgba(40, 52, 71, 0.5);
|
||||
|
||||
/**
|
||||
* Make the sidebar flush with the top of the toolbar. Take the size of
|
||||
* the toolbar, plus its padding, and subtract from 100%.
|
||||
*/
|
||||
height: calc(100% - #{$newToolbarSize} - 10px);
|
||||
left: 0;
|
||||
|
||||
.side-toolbar-close {
|
||||
background: gray;
|
||||
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 100%;
|
||||
color: white;
|
||||
cursor:pointer;
|
||||
height: 10px;
|
||||
line-height: 10px;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
text-align: center;
|
||||
top: 5px;
|
||||
width: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#chatconversation {
|
||||
top: 15px;
|
||||
}
|
||||
}
|
||||
#sideToolbarContainer {
|
||||
background-color: $sideToolbarContainerBg;
|
||||
height: 100%;
|
||||
|
|
|
@ -261,6 +261,217 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: when the old filmstrip has been removed, remove the "new-" prefix.
|
||||
*/
|
||||
.new-toolbox {
|
||||
background-color: rgba(40, 52, 71, 0.5);
|
||||
bottom: calc((#{$newToolbarSize} * 2) * -1);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 20px;
|
||||
position: absolute;
|
||||
transition: bottom .3s ease-in;
|
||||
width: 100%;
|
||||
z-index: $toolbarZ;
|
||||
|
||||
&.visible {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&.no-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button-group-center,
|
||||
.button-group-left,
|
||||
.button-group-right {
|
||||
display: flex;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.button-group-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-group-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite font-awesome styling to match jitsi-icon styling.
|
||||
*/
|
||||
.fa {
|
||||
font-size: 1.22em;
|
||||
}
|
||||
|
||||
i {
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 100%;
|
||||
line-height: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: rgba(40, 52, 71, 0.7);
|
||||
}
|
||||
|
||||
i.toggled {
|
||||
background: rgba(40, 52, 71, 1);
|
||||
}
|
||||
|
||||
i.toggled:hover {
|
||||
background-color: rgba(40, 52, 71, 1);
|
||||
}
|
||||
|
||||
i.disabled {
|
||||
cursor: initial
|
||||
}
|
||||
|
||||
i.disabled:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
.icon-hangup {
|
||||
color: $hangupColor;
|
||||
}
|
||||
|
||||
.overflow-menu {
|
||||
font-size: 1.2em;
|
||||
list-style-type: none;
|
||||
/**
|
||||
* Undo atlaskit padding by reducing margins.
|
||||
*/
|
||||
margin: -15px -24px;
|
||||
padding: 0;
|
||||
|
||||
.overflow-menu-item {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
&.unclickable {
|
||||
cursor: default;
|
||||
}
|
||||
&.unclickable:hover {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.overflow-menu-item-icon {
|
||||
margin-right: 10px;
|
||||
|
||||
i {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-text {
|
||||
max-width: 150px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-button {
|
||||
color: $toolbarButtonColor;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: $newToolbarFontSize;
|
||||
line-height: $newToolbarSize;
|
||||
margin: 0 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toolbar-button-with-badge {
|
||||
position: relative;
|
||||
|
||||
.badge-round {
|
||||
bottom: 9px;
|
||||
position: absolute;
|
||||
right: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-button-wth-dialog {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
height: $newToolbarSize;
|
||||
width: $newToolbarSize;
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip-toolbox {
|
||||
background-color: rgba(40, 52, 71, 0.5);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: $toolbarZ;
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: $newToolbarFontSize;
|
||||
height: 37px;
|
||||
line-height: 37px;
|
||||
width: 37px;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: rgba(40, 52, 71, 0.7);
|
||||
}
|
||||
|
||||
i.toggled {
|
||||
background: rgba(40, 52, 71, 1);
|
||||
}
|
||||
|
||||
i.toggled:hover {
|
||||
background-color: rgba(40, 52, 71, 1);
|
||||
}
|
||||
|
||||
.icon-hangup {
|
||||
color: $hangupColor;
|
||||
}
|
||||
|
||||
.toolbox-button {
|
||||
color: $toolbarButtonColor;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
border-radius: 3px;
|
||||
|
||||
.toolbox-button:first-child i {
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
.toolbox-button:last-child i {
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip-only {
|
||||
.toolbox,
|
||||
.toolbox-toolbars {
|
||||
|
|
|
@ -36,6 +36,8 @@ $alwaysOnTopToolbarFontSize: 1em;
|
|||
$alwaysOnTopToolbarSize: 30px;
|
||||
$defaultToolbarSize: 50px;
|
||||
$defaultFilmStripOnlyToolbarSize: 37px;
|
||||
$newToolbarSize: 50px;
|
||||
$newToolbarFontSize: 1.9em;
|
||||
$secToolbarFontSize: 1.9em;
|
||||
$secToolbarLineHeight: 45px;
|
||||
$toolbarAvatarPadding: 10px;
|
||||
|
|
|
@ -19,6 +19,20 @@
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
&.use-new-toolbox {
|
||||
/**
|
||||
* Adjust the height of the filmstrip as the toolbar is displayed.
|
||||
*/
|
||||
.filmstrip {
|
||||
top: 0;
|
||||
transition: height .3s ease-in;
|
||||
|
||||
&.reduce-height {
|
||||
height: calc(100% - #{$newToolbarSize});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
align-items: flex-end;
|
||||
box-sizing: border-box;
|
||||
|
@ -32,14 +46,7 @@
|
|||
* any parent is also fixed.
|
||||
*/
|
||||
position: fixed;
|
||||
|
||||
/**
|
||||
* z-index adjusting is needed because the video state indicator has to
|
||||
* display over the filmstrip when no videos are displayed but still be
|
||||
* clickable but its inline dialogs must display over the video state
|
||||
* indicator when videos are displayed.
|
||||
*/
|
||||
z-index: #{$tooltipsZ + 1};
|
||||
z-index: $filmstripVideosZ;
|
||||
|
||||
/**
|
||||
* Hide videos by making them slight to the right.
|
||||
|
|
|
@ -135,6 +135,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.modal-dialog-form {
|
||||
.video-quality-dialog-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.video-state-indicator {
|
||||
background: $videoStateIndicatorBackground;
|
||||
cursor: default;
|
||||
|
@ -162,11 +168,11 @@
|
|||
}
|
||||
|
||||
.centeredVideoLabel.moveToCorner {
|
||||
z-index: $tooltipsZ;
|
||||
z-index: $zindex3;
|
||||
}
|
||||
|
||||
#videoResolutionLabel {
|
||||
z-index: #{$tooltipsZ + 1};
|
||||
z-index: $zindex3 + 1;
|
||||
}
|
||||
|
||||
.centeredVideoLabel {
|
||||
|
|
BIN
fonts/jitsi.eot
BIN
fonts/jitsi.eot
Binary file not shown.
|
@ -23,6 +23,7 @@
|
|||
<glyph unicode="" glyph-name="event_note" d="M598 426v-84h-300v84h300zM810 214v468h-596v-468h596zM810 896c46 0 86-40 86-86v-596c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h42v86h86v-86h340v86h86v-86h42zM726 598v-86h-428v86h428z" />
|
||||
<glyph unicode="" glyph-name="phone-talk" d="M640 512c0 70-58 128-128 128v86c118 0 214-96 214-214h-86zM810 512c0 166-132 298-298 298v86c212 0 384-172 384-384h-86zM854 362c24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44l-94-94c62-122 162-220 282-282l94 94c12 12 30 14 44 10 48-16 98-24 152-24z" />
|
||||
<glyph unicode="" glyph-name="public" d="M764 282c56 60 90 142 90 230 0 142-88 266-214 316v-18c0-46-40-84-86-84h-84v-86c0-24-20-42-44-42h-84v-86h256c24 0 42-18 42-42v-128h42c38 0 70-26 82-60zM470 174v82c-46 0-86 40-86 86v42l-204 204c-6-24-10-50-10-76 0-174 132-318 300-338zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
|
||||
<glyph unicode="" glyph-name="open_in_new" d="M598 896h298v-298h-86v152l-418-418-60 60 418 418h-152v86zM810 214v298h86v-298c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h298v-86h-298v-596h596z" />
|
||||
<glyph unicode="" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
|
||||
<glyph unicode="" glyph-name="avatar" d="M512 204c106 0 200 56 256 138-2 84-172 132-256 132-86 0-254-48-256-132 56-82 150-138 256-138zM512 810c-70 0-128-58-128-128s58-128 128-128 128 58 128 128-58 128-128 128zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
|
||||
<glyph unicode="" glyph-name="download" d="M726 470h-128v170h-172v-170h-128l214-214zM826 596c110-8 198-100 198-212 0-118-96-214-214-214h-554c-142 0-256 114-256 256 0 132 100 240 228 254 54 102 160 174 284 174 156 0 284-110 314-258z" />
|
||||
|
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.ttf
Binary file not shown.
BIN
fonts/jitsi.woff
BIN
fonts/jitsi.woff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -44,7 +44,10 @@ var interfaceConfig = {
|
|||
'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup',
|
||||
|
||||
// extended toolbar
|
||||
'profile', 'contacts', 'info', 'chat', 'recording', 'etherpad', 'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip' ],
|
||||
'profile', 'contacts', 'info', 'chat', 'recording', 'etherpad',
|
||||
'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip',
|
||||
'invite', 'feedback', 'stats', 'shortcuts'
|
||||
],
|
||||
|
||||
/**
|
||||
* Main Toolbar Buttons
|
||||
|
@ -150,7 +153,18 @@ var interfaceConfig = {
|
|||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
VIDEO_QUALITY_LABEL_DISABLED: false
|
||||
VIDEO_QUALITY_LABEL_DISABLED: false,
|
||||
|
||||
/**
|
||||
* This is a temporary feature flag used to gate access to the toolbox so it
|
||||
* can be developed through smaller changesets. This feature flag will be
|
||||
* removed at some point, as well as the old toolbox. This new toolbox will
|
||||
* be horizontal and support for horizontal filmstrip will be removed,
|
||||
* except in the case of interfaceConfig.filmStripOnly being true.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
_USE_NEW_TOOLBOX: false
|
||||
|
||||
/**
|
||||
* Specify custom URL for downloading android mobile app.
|
||||
|
|
|
@ -73,14 +73,23 @@
|
|||
"toolbar": {
|
||||
"addPeople": "Add people to your call",
|
||||
"audioonly": "Enable / Disable audio only mode (saves bandwidth)",
|
||||
"callQuality": "Manage call quality",
|
||||
"enterFullScreen": "View full screen",
|
||||
"exitFullScreen": "Exit full screen",
|
||||
"feedback": "Leave feedback",
|
||||
"moreActions": "More actions",
|
||||
"mute": "Mute / Unmute",
|
||||
"videomute": "Start / Stop camera",
|
||||
"authenticate": "Authenticate",
|
||||
"lock": "Lock / Unlock room",
|
||||
"chat": "Open / Close chat",
|
||||
"etherpad": "Open / Close shared document",
|
||||
"documentOpen": "Open shared document",
|
||||
"documentClose": "Close shared document",
|
||||
"sharedvideo": "Share a YouTube video",
|
||||
"sharescreen": "Start / Stop screen sharing",
|
||||
"sharescreen": "Screen share",
|
||||
"sharescreenDisabled": "Screen share disabled",
|
||||
"stopSharedVideo": "Stop YouTube video",
|
||||
"fullscreen": "View / Exit full screen",
|
||||
"sip": "Call SIP number",
|
||||
"Settings": "Settings",
|
||||
|
@ -96,7 +105,9 @@
|
|||
"micDisabled": "Microphone is not available",
|
||||
"filmstrip": "Show / Hide videos",
|
||||
"profile": "Edit your profile",
|
||||
"raiseHand": "Raise / Lower your hand"
|
||||
"raiseHand": "Raise / Lower your hand",
|
||||
"shortcuts": "View shortcuts",
|
||||
"speakerStats": "Speaker stats"
|
||||
},
|
||||
"unsupportedBrowser": {
|
||||
"appNotInstalled": "Join this meeting with __app__ on your phone.",
|
||||
|
@ -285,6 +296,7 @@
|
|||
"liveStreaming": "Live Streaming",
|
||||
"streamKey": "Live stream key",
|
||||
"startLiveStreaming": "Go live now",
|
||||
"startRecording": "Start recording",
|
||||
"stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
|
||||
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
|
||||
"stopLiveStreaming": "Stop live streaming",
|
||||
|
@ -473,6 +485,7 @@
|
|||
"loadingPeople": "Searching for people to invite",
|
||||
"noResults": "No matching search results",
|
||||
"noValidNumbers": "Please enter a phone number",
|
||||
"notAvailable": "You can't invite people.",
|
||||
"searchNumbers": "Enter a phone number to invite",
|
||||
"searchPeople": "Enter a name to invite",
|
||||
"searchPeopleAndNumbers": "Enter a name or phone number to invite",
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
} from '../../react/features/base/participants';
|
||||
import { destroyLocalTracks } from '../../react/features/base/tracks';
|
||||
import { openDisplayNamePrompt } from '../../react/features/display-name';
|
||||
import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
|
||||
import {
|
||||
setNotificationsEnabled,
|
||||
showWarningNotification
|
||||
|
@ -100,9 +101,6 @@ const UIListeners = new Map([
|
|||
], [
|
||||
UIEvents.SHARED_VIDEO_CLICKED,
|
||||
() => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
|
||||
], [
|
||||
UIEvents.TOGGLE_FULLSCREEN,
|
||||
() => UI.toggleFullScreen()
|
||||
], [
|
||||
UIEvents.TOGGLE_CHAT,
|
||||
() => UI.toggleChat()
|
||||
|
@ -135,14 +133,6 @@ const UIListeners = new Map([
|
|||
]
|
||||
]);
|
||||
|
||||
/**
|
||||
* Toggles the application in and out of full screen mode
|
||||
* (a.k.a. presentation mode in Chrome).
|
||||
*/
|
||||
UI.toggleFullScreen = function() {
|
||||
UIUtil.isFullScreen() ? UIUtil.exitFullScreen() : UIUtil.enterFullScreen();
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicates if we're currently in full screen mode.
|
||||
*
|
||||
|
@ -255,12 +245,20 @@ UI.showLocalConnectionInterrupted = function(isInterrupted) {
|
|||
|
||||
/**
|
||||
* Sets the "raised hand" status for a participant.
|
||||
*
|
||||
* @param {string} id - The id of the participant whose raised hand UI should
|
||||
* be updated.
|
||||
* @param {string} name - The name of the participant with the raised hand
|
||||
* update.
|
||||
* @param {boolean} raisedHandStatus - Whether the participant's hand is raised
|
||||
* or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.setRaisedHandStatus = (participant, raisedHandStatus) => {
|
||||
VideoLayout.setRaisedHandStatus(participant.getId(), raisedHandStatus);
|
||||
UI.setRaisedHandStatus = (id, name, raisedHandStatus) => {
|
||||
VideoLayout.setRaisedHandStatus(id, raisedHandStatus);
|
||||
if (raisedHandStatus) {
|
||||
messageHandler.participantNotification(
|
||||
participant.getDisplayName(),
|
||||
name,
|
||||
'notify.somebody',
|
||||
'connected',
|
||||
'notify.raisedHand');
|
||||
|
@ -374,6 +372,14 @@ UI.start = function() {
|
|||
$('body').addClass('vertical-filmstrip');
|
||||
}
|
||||
|
||||
|
||||
// TODO: remove this class once the old toolbar has been removed. This class
|
||||
// is set so that any CSS changes needed to adjust elements outside of the
|
||||
// new toolbar can be scoped to just the app with the new toolbar enabled.
|
||||
if (interfaceConfig._USE_NEW_TOOLBOX && !interfaceConfig.filmStripOnly) {
|
||||
$('body').addClass('use-new-toolbox');
|
||||
}
|
||||
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
};
|
||||
|
||||
|
@ -404,12 +410,7 @@ UI.bindEvents = () => {
|
|||
// Resize and reposition videos in full screen mode.
|
||||
$(document).on(
|
||||
'webkitfullscreenchange mozfullscreenchange fullscreenchange',
|
||||
() => {
|
||||
eventEmitter.emit(
|
||||
UIEvents.FULLSCREEN_TOGGLED,
|
||||
UIUtil.isFullScreen());
|
||||
onResize();
|
||||
});
|
||||
onResize);
|
||||
|
||||
$(window).resize(onResize);
|
||||
};
|
||||
|
@ -474,6 +475,7 @@ UI.initEtherpad = name => {
|
|||
etherpadManager
|
||||
= new EtherpadManager(config.etherpad_base, name, eventEmitter);
|
||||
|
||||
APP.store.dispatch(setEtherpadHasInitialzied());
|
||||
APP.store.dispatch(showEtherpadButton());
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
/* global $, interfaceConfig */
|
||||
/* global $, APP, interfaceConfig */
|
||||
|
||||
import { setDocumentEditingState } from '../../../react/features/etherpad';
|
||||
import { getToolboxHeight } from '../../../react/features/toolbox';
|
||||
|
||||
import VideoLayout from '../videolayout/VideoLayout';
|
||||
import LargeContainer from '../videolayout/LargeContainer';
|
||||
|
@ -126,7 +129,8 @@ class Etherpad extends LargeContainer {
|
|||
let height, width;
|
||||
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
height = containerHeight;
|
||||
height = interfaceConfig._USE_NEW_TOOLBOX
|
||||
? containerHeight - getToolboxHeight() : containerHeight;
|
||||
width = containerWidth - Filmstrip.getFilmstripWidth();
|
||||
} else {
|
||||
height = containerHeight - Filmstrip.getFilmstripHeight();
|
||||
|
@ -242,5 +246,7 @@ export default class EtherpadManager {
|
|||
|
||||
this.eventEmitter
|
||||
.emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible);
|
||||
|
||||
APP.store.dispatch(setDocumentEditingState(!isVisible));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import {
|
|||
StartLiveStreamDialog,
|
||||
StopLiveStreamDialog,
|
||||
hideRecordingLabel,
|
||||
setRecordingType,
|
||||
updateRecordingState
|
||||
} from '../../../react/features/recording';
|
||||
|
||||
|
@ -202,6 +203,8 @@ const Recording = {
|
|||
this.eventEmitter = eventEmitter;
|
||||
this.recordingType = recordingType;
|
||||
|
||||
APP.store.dispatch(setRecordingType(recordingType));
|
||||
|
||||
this.updateRecordingState(APP.conference.getRecordingState());
|
||||
|
||||
if (recordingType === 'jibri') {
|
||||
|
@ -219,6 +222,9 @@ const Recording = {
|
|||
'#toolbar_button_record',
|
||||
ev => this._onToolbarButtonClick(ev));
|
||||
|
||||
this.eventEmitter.on(UIEvents.TOGGLE_RECORDING,
|
||||
() => this._onToolbarButtonClick());
|
||||
|
||||
// If I am a recorder then I publish my recorder custom role to notify
|
||||
// everyone.
|
||||
if (config.iAmRecorder) {
|
||||
|
@ -287,6 +293,7 @@ const Recording = {
|
|||
this.currentState = recordingState;
|
||||
|
||||
let labelDisplayConfiguration;
|
||||
let isRecording = false;
|
||||
|
||||
switch (recordingState) {
|
||||
case JitsiRecordingStatus.ON:
|
||||
|
@ -298,6 +305,7 @@ const Recording = {
|
|||
};
|
||||
|
||||
this._setToolbarButtonToggled(true);
|
||||
isRecording = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
@ -362,6 +370,7 @@ const Recording = {
|
|||
}
|
||||
|
||||
APP.store.dispatch(updateRecordingState({
|
||||
isRecording,
|
||||
labelDisplayConfiguration,
|
||||
recordingState
|
||||
}));
|
||||
|
|
|
@ -18,7 +18,11 @@ import {
|
|||
participantJoined,
|
||||
participantLeft
|
||||
} from '../../../react/features/base/participants';
|
||||
import { dockToolbox, showToolbox } from '../../../react/features/toolbox';
|
||||
import {
|
||||
dockToolbox,
|
||||
getToolboxHeight,
|
||||
showToolbox
|
||||
} from '../../../react/features/toolbox';
|
||||
|
||||
import SharedVideoThumb from './SharedVideoThumb';
|
||||
|
||||
|
@ -695,7 +699,8 @@ class SharedVideoContainer extends LargeContainer {
|
|||
let height, width;
|
||||
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
height = containerHeight;
|
||||
height = interfaceConfig._USE_NEW_TOOLBOX
|
||||
? containerHeight - getToolboxHeight() : containerHeight;
|
||||
width = containerWidth - Filmstrip.getFilmstripWidth();
|
||||
} else {
|
||||
height = containerHeight - Filmstrip.getFilmstripHeight();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* global $ */
|
||||
/* global $, APP */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import { setVisiblePanel } from '../../../react/features/side-panel';
|
||||
|
||||
/**
|
||||
* Handles open and close of the extended toolbar side panel
|
||||
|
@ -57,6 +58,7 @@ const SideContainerToggler = {
|
|||
|
||||
if (isSelectorVisible) {
|
||||
this.hide();
|
||||
APP.store.dispatch(setVisiblePanel(null));
|
||||
} else {
|
||||
if (this.isVisible()) {
|
||||
$('#sideToolbarContainer').children()
|
||||
|
@ -74,6 +76,7 @@ const SideContainerToggler = {
|
|||
}
|
||||
|
||||
this.showInnerContainer(elementSelector);
|
||||
APP.store.dispatch(setVisiblePanel(elementId));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global APP, $ */
|
||||
/* global APP, $, interfaceConfig */
|
||||
|
||||
import { processReplacements, linkify } from './Replacement';
|
||||
import CommandsProcessor from './Commands';
|
||||
|
@ -9,7 +9,12 @@ import UIEvents from '../../../../service/UI/UIEvents';
|
|||
|
||||
import { smileys } from './smileys';
|
||||
|
||||
import { dockToolbox, setSubject } from '../../../../react/features/toolbox';
|
||||
import { addMessage, markAllRead } from '../../../../react/features/chat';
|
||||
import {
|
||||
dockToolbox,
|
||||
getToolboxHeight,
|
||||
setSubject
|
||||
} from '../../../../react/features/toolbox';
|
||||
|
||||
let unreadMessages = 0;
|
||||
const sidePanelsContainerId = 'sideToolbarContainer';
|
||||
|
@ -163,6 +168,8 @@ function addSmileys() {
|
|||
* Resizes the chat conversation.
|
||||
*/
|
||||
function resizeChatConversation() {
|
||||
// FIXME: this function can all be done with CSS. If Chat is ever rewritten,
|
||||
// do not copy over this logic.
|
||||
const msgareaHeight = $('#usermsg').outerHeight();
|
||||
const chatspace = $(`#${CHAT_CONTAINER_ID}`);
|
||||
const width = chatspace.width();
|
||||
|
@ -173,7 +180,16 @@ function resizeChatConversation() {
|
|||
$('#smileys').css('bottom', (msgareaHeight - 26) / 2);
|
||||
$('#smileysContainer').css('bottom', msgareaHeight);
|
||||
chat.width(width - 10);
|
||||
|
||||
if (interfaceConfig._USE_NEW_TOOLBOX) {
|
||||
const maybeAMagicNumberForPaddingAndMargin = 100;
|
||||
const offset = maybeAMagicNumberForPaddingAndMargin
|
||||
+ msgareaHeight + getToolboxHeight();
|
||||
|
||||
chat.height(window.innerHeight - offset);
|
||||
} else {
|
||||
chat.height(window.innerHeight - 15 - msgareaHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -249,6 +265,7 @@ const Chat = {
|
|||
}
|
||||
|
||||
unreadMessages = 0;
|
||||
APP.store.dispatch(markAllRead());
|
||||
updateVisualNotification();
|
||||
|
||||
// Undock the toolbar when the chat is shown and if we're in a
|
||||
|
@ -274,9 +291,10 @@ const Chat = {
|
|||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
updateChatConversation(id, displayName, message, stamp) {
|
||||
const isFromLocalParticipant = APP.conference.isLocalId(id);
|
||||
let divClassName = '';
|
||||
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
if (isFromLocalParticipant) {
|
||||
divClassName = 'localuser';
|
||||
} else {
|
||||
divClassName = 'remoteuser';
|
||||
|
@ -294,6 +312,7 @@ const Chat = {
|
|||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br/>');
|
||||
const escDisplayName = UIUtil.escapeHtml(displayName);
|
||||
const timestamp = getCurrentTime(stamp);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message = processReplacements(escMessage);
|
||||
|
@ -302,13 +321,18 @@ const Chat = {
|
|||
= `${'<div class="chatmessage">'
|
||||
+ '<img src="images/chatArrow.svg" class="chatArrow">'
|
||||
+ '<div class="username '}${divClassName}">${escDisplayName
|
||||
}</div><div class="timestamp">${getCurrentTime(stamp)
|
||||
}</div><div class="timestamp">${timestamp
|
||||
}</div><div class="usermessage">${message}</div>`
|
||||
+ '</div>';
|
||||
|
||||
$('#chatconversation').append(messageContainer);
|
||||
$('#chatconversation').animate(
|
||||
{ scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
|
||||
|
||||
const markAsRead = Chat.isVisible() || isFromLocalParticipant;
|
||||
|
||||
APP.store.dispatch(addMessage(
|
||||
escDisplayName, message, timestamp, markAsRead));
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -227,43 +227,10 @@ const UIUtil = {
|
|||
* mode, {false} otherwise
|
||||
*/
|
||||
isFullScreen() {
|
||||
return document.fullscreenElement
|
||||
return Boolean(document.fullscreenElement
|
||||
|| document.mozFullScreenElement
|
||||
|| document.webkitFullscreenElement
|
||||
|| document.msFullscreenElement;
|
||||
},
|
||||
|
||||
/**
|
||||
* Exits full screen mode.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
|
||||
*/
|
||||
exitFullScreen() {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Enter full screen mode.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
|
||||
*/
|
||||
enterFullScreen() {
|
||||
if (document.documentElement.requestFullscreen) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else if (document.documentElement.msRequestFullscreen) {
|
||||
document.documentElement.msRequestFullscreen();
|
||||
} else if (document.documentElement.mozRequestFullScreen) {
|
||||
document.documentElement.mozRequestFullScreen();
|
||||
} else if (document.documentElement.webkitRequestFullscreen) {
|
||||
document.documentElement
|
||||
.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
|
||||
}
|
||||
|| document.msFullscreenElement);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -90,6 +90,17 @@ const KeyboardShortcut = {
|
|||
enabled = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the {@KeyboardShortcutsDialog} dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
openDialog() {
|
||||
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
|
||||
shortcutDescriptions: _shortcutsHelp
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a new shortcut.
|
||||
*
|
||||
|
@ -177,9 +188,7 @@ const KeyboardShortcut = {
|
|||
_initGlobalShortcuts() {
|
||||
this.registerShortcut('?', null, () => {
|
||||
sendAnalytics(createShortcutEvent('help'));
|
||||
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
|
||||
shortcutDescriptions: _shortcutsHelp
|
||||
}));
|
||||
this.openDialog();
|
||||
}, 'keyboardShortcuts.toggleShortcuts');
|
||||
|
||||
// register SPACE shortcut in two steps to insure visibility of help
|
||||
|
|
|
@ -96,6 +96,18 @@ export const P2P_STATUS_CHANGED = Symbol('P2P_STATUS_CHANGED');
|
|||
*/
|
||||
export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the desktop sharing enabled flag for
|
||||
* the current conference.
|
||||
*
|
||||
* {
|
||||
* type: SET_DESKTOP_SHARING_ENABLED,
|
||||
* desktopSharingEnabled: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_DESKTOP_SHARING_ENABLED
|
||||
= Symbol('SET_DESKTOP_SHARING_ENABLED');
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the current known status of the
|
||||
* Follow Me feature.
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
LOCK_STATE_CHANGED,
|
||||
P2P_STATUS_CHANGED,
|
||||
SET_AUDIO_ONLY,
|
||||
SET_DESKTOP_SHARING_ENABLED,
|
||||
SET_FOLLOW_ME,
|
||||
SET_LASTN,
|
||||
SET_PASSWORD,
|
||||
|
@ -433,6 +434,22 @@ export function setAudioOnly(audioOnly: boolean) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flag for indicating if desktop sharing is enabled.
|
||||
*
|
||||
* @param {boolean} desktopSharingEnabled - True if desktop sharing is enabled.
|
||||
* @returns {{
|
||||
* type: SET_DESKTOP_SHARING_ENABLED,
|
||||
* desktopSharingEnabled: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) {
|
||||
return {
|
||||
type: SET_DESKTOP_SHARING_ENABLED,
|
||||
desktopSharingEnabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the Follow Me feature.
|
||||
*
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
LOCK_STATE_CHANGED,
|
||||
P2P_STATUS_CHANGED,
|
||||
SET_AUDIO_ONLY,
|
||||
SET_DESKTOP_SHARING_ENABLED,
|
||||
SET_FOLLOW_ME,
|
||||
SET_PASSWORD,
|
||||
SET_RECEIVE_VIDEO_QUALITY,
|
||||
|
@ -57,6 +58,9 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
|
|||
case SET_AUDIO_ONLY:
|
||||
return _setAudioOnly(state, action);
|
||||
|
||||
case SET_DESKTOP_SHARING_ENABLED:
|
||||
return _setDesktopSharingEnabled(state, action);
|
||||
|
||||
case SET_FOLLOW_ME:
|
||||
return {
|
||||
...state,
|
||||
|
@ -329,6 +333,21 @@ function _setAudioOnly(state, action) {
|
|||
return set(state, 'audioOnly', action.audioOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action SET_DESKTOP_SHARING_ENABLED of the feature
|
||||
* base/conference.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/conference.
|
||||
* @param {Action} action - The Redux action SET_DESKTOP_SHARING_ENABLED to
|
||||
* reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _setDesktopSharingEnabled(state, action) {
|
||||
return set(state, 'desktopSharingEnabled', action.desktopSharingEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
|
||||
*
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* @flow */
|
||||
// @flow
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
|
@ -54,10 +54,12 @@ export function connect() {
|
|||
/**
|
||||
* Closes connection.
|
||||
*
|
||||
* @param {boolean} [requestFeedback] - Whether or not to attempt showing a
|
||||
* request for call feedback.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function disconnect() {
|
||||
export function disconnect(requestFeedback: boolean = false) {
|
||||
// XXX For web based version we use conference hanging up logic from the old
|
||||
// app.
|
||||
return () => APP.conference.hangup();
|
||||
return () => APP.conference.hangup(requestFeedback);
|
||||
}
|
||||
|
|
|
@ -39,6 +39,12 @@ type Props = {
|
|||
*/
|
||||
disableBlanketClickDismiss: boolean,
|
||||
|
||||
/**
|
||||
* If true, the cancel button will not display but cancel actions, like
|
||||
* clicking the blanket, will cancel.
|
||||
*/
|
||||
hideCancelButton: boolean,
|
||||
|
||||
/**
|
||||
* Whether the dialog is modal. This means clicking on the blanket will
|
||||
* leave the dialog open. No cancel button.
|
||||
|
@ -263,7 +269,9 @@ class StatelessDialog extends Component<Props> {
|
|||
* not modal.
|
||||
*/
|
||||
_renderCancelButton(options = {}) {
|
||||
if (options.cancelDisabled || options.isModal) {
|
||||
if (options.cancelDisabled
|
||||
|| options.isModal
|
||||
|| options.hideCancelButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
|||
/* @flow */
|
||||
// @flow
|
||||
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
|
@ -10,8 +10,12 @@ import {
|
|||
import { MiddlewareRegistry } from '../redux';
|
||||
import { playSound, registerSound, unregisterSound } from '../sounds';
|
||||
|
||||
import { localParticipantIdChanged } from './actions';
|
||||
import {
|
||||
localParticipantIdChanged,
|
||||
participantUpdated
|
||||
} from './actions';
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
KICK_PARTICIPANT,
|
||||
MUTE_REMOTE_PARTICIPANT,
|
||||
PARTICIPANT_DISPLAY_NAME_CHANGED,
|
||||
|
@ -27,6 +31,7 @@ import {
|
|||
import {
|
||||
getAvatarURLByParticipantId,
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount
|
||||
} from './functions';
|
||||
import {
|
||||
|
@ -66,6 +71,27 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
store.dispatch(localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
|
||||
break;
|
||||
|
||||
case DOMINANT_SPEAKER_CHANGED: {
|
||||
// Ensure the raised hand state is cleared for the dominant speaker.
|
||||
const participant = getLocalParticipant(store.getState());
|
||||
|
||||
if (participant) {
|
||||
const local = participant.id === action.participant.id;
|
||||
|
||||
store.dispatch(participantUpdated({
|
||||
id: action.participant.id,
|
||||
local,
|
||||
raisedHand: false
|
||||
}));
|
||||
}
|
||||
|
||||
if (typeof APP === 'object') {
|
||||
APP.UI.markDominantSpeaker(action.participant.id);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case KICK_PARTICIPANT:
|
||||
conference.kickParticipant(action.id);
|
||||
break;
|
||||
|
@ -90,10 +116,37 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
case PARTICIPANT_JOINED:
|
||||
case PARTICIPANT_UPDATED: {
|
||||
if (typeof APP !== 'undefined') {
|
||||
const participant = action.participant;
|
||||
const { id, local } = participant;
|
||||
const { participant } = action;
|
||||
const { id, local, raisedHand } = participant;
|
||||
|
||||
// Send an external update of the local participant's raised hand state
|
||||
// if a new raised hand state is defined in the action.
|
||||
if (typeof raisedHand !== 'undefined') {
|
||||
if (local) {
|
||||
conference.setLocalParticipantProperty(
|
||||
'raisedHand',
|
||||
raisedHand);
|
||||
}
|
||||
|
||||
if (typeof APP === 'object') {
|
||||
if (local) {
|
||||
APP.UI.onLocalRaiseHandChanged(raisedHand);
|
||||
APP.UI.setLocalRaisedHandStatus(raisedHand);
|
||||
} else {
|
||||
const remoteParticipant
|
||||
= getParticipantById(store.getState(), id);
|
||||
|
||||
remoteParticipant
|
||||
&& APP.UI.setRaisedHandStatus(
|
||||
remoteParticipant.id,
|
||||
remoteParticipant.name,
|
||||
raisedHand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify external listeners of potential avatarURL changes.
|
||||
if (typeof APP === 'object') {
|
||||
const preUpdateAvatarURL
|
||||
= getAvatarURLByParticipantId(store.getState(), id);
|
||||
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
/**
|
||||
* The type of redux action dispatched to disable screensharing or to start the
|
||||
* flow for enabling screenshare.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_SCREENSHARING
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_SCREENSHARING = Symbol('TOGGLE_SCREENSHARING');
|
||||
|
||||
/**
|
||||
* The type of redux action dispatched when a track has been (locally or
|
||||
* remotely) added to the conference.
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
import { getLocalParticipant } from '../participants';
|
||||
|
||||
import {
|
||||
TOGGLE_SCREENSHARING,
|
||||
TRACK_ADDED,
|
||||
TRACK_CREATE_CANCELED,
|
||||
TRACK_CREATE_ERROR,
|
||||
|
@ -172,6 +173,20 @@ export function destroyLocalTracks() {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the local participant is ending screensharing or beginning the
|
||||
* screensharing flow.
|
||||
*
|
||||
* @returns {{
|
||||
* type: TOGGLE_SCREENSHARING,
|
||||
* }}
|
||||
*/
|
||||
export function toggleScreensharing() {
|
||||
return {
|
||||
type: TOGGLE_SCREENSHARING
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces one track with another for one renegotiation instead of invoking
|
||||
* two renegotiations with a separate removeTrack and addTrack. Disposes the
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* @flow */
|
||||
// @flow
|
||||
|
||||
import {
|
||||
CAMERA_FACING_MODE,
|
||||
|
@ -10,9 +10,15 @@ import {
|
|||
toggleCameraFacingMode
|
||||
} from '../media';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
import { createLocalTracksA } from './actions';
|
||||
import { TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from './actionTypes';
|
||||
import {
|
||||
TOGGLE_SCREENSHARING,
|
||||
TRACK_ADDED,
|
||||
TRACK_REMOVED,
|
||||
TRACK_UPDATED
|
||||
} from './actionTypes';
|
||||
import { getLocalTrack, setTrackMuted } from './functions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
@ -81,6 +87,12 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
break;
|
||||
}
|
||||
|
||||
case TOGGLE_SCREENSHARING:
|
||||
if (typeof APP === 'object') {
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
|
||||
}
|
||||
break;
|
||||
|
||||
case TRACK_ADDED:
|
||||
// TODO Remove this middleware case once all UI interested in new tracks
|
||||
// being added are converted to react and listening for store changes.
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* The type of the action which signals to add a new chat message.
|
||||
*
|
||||
* {
|
||||
* type: ADD_MESSAGE,
|
||||
* hasRead: boolean,
|
||||
* message: string,
|
||||
* timestamp: string,
|
||||
* userName: string
|
||||
* }
|
||||
*/
|
||||
export const ADD_MESSAGE = Symbol('ADD_MESSAGE');
|
||||
|
||||
/**
|
||||
* The type of the action which updates which is the most recent message that
|
||||
* has been seen by the local participant.
|
||||
*
|
||||
* {
|
||||
* type: SET_LAST_READ_MESSAGE,
|
||||
* message: Object
|
||||
* }
|
||||
*/
|
||||
export const SET_LAST_READ_MESSAGE = Symbol('SET_LAST_READ_MESSAGE');
|
|
@ -0,0 +1,63 @@
|
|||
import { ADD_MESSAGE, SET_LAST_READ_MESSAGE } from './actionTypes';
|
||||
|
||||
/* eslint-disable max-params */
|
||||
|
||||
/**
|
||||
* Adds a chat message to the collection of messages.
|
||||
*
|
||||
* @param {string} userName - The username to display of the participant that
|
||||
* authored the message.
|
||||
* @param {string} message - The received message to display.
|
||||
* @param {string} timestamp - A timestamp to display for when the message was
|
||||
* received.
|
||||
* @param {boolean} hasRead - Whether or not to immediately mark the message as
|
||||
* read.
|
||||
* @returns {{
|
||||
* type: ADD_MESSAGE,
|
||||
* hasRead: boolean,
|
||||
* message: string,
|
||||
* timestamp: string,
|
||||
* userName: string
|
||||
* }}
|
||||
*/
|
||||
export function addMessage(userName, message, timestamp, hasRead) {
|
||||
return {
|
||||
type: ADD_MESSAGE,
|
||||
hasRead,
|
||||
message,
|
||||
timestamp,
|
||||
userName
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-enable max-params */
|
||||
|
||||
/**
|
||||
* Sets the last read message cursor to the latest message.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function markAllRead() {
|
||||
return (dispatch, getState) => {
|
||||
const { messages } = getState()['features/chat'];
|
||||
|
||||
dispatch(setLastReadMessage(messages[messages.length - 1]));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the last read message cursor to be set at the passed in message. The
|
||||
* assumption is that messages will be ordered chronologically.
|
||||
*
|
||||
* @param {Object} message - The message from the redux state.
|
||||
* @returns {{
|
||||
* type: SET_LAST_READ_MESSAGE,
|
||||
* message: Object
|
||||
* }}
|
||||
*/
|
||||
export function setLastReadMessage(message) {
|
||||
return {
|
||||
type: SET_LAST_READ_MESSAGE,
|
||||
message
|
||||
};
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getUnreadCount } from '../functions';
|
||||
|
||||
/**
|
||||
* FIXME: Move this UI logic to a generic component that can be used for
|
||||
* {@code ParticipantCounter} as well.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a count of the number of
|
||||
* unread chat messages.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class ChatCounter extends Component {
|
||||
static propTypes = {
|
||||
/**
|
||||
* The number of unread chat messages in the conference.
|
||||
*/
|
||||
_count: PropTypes.number
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<span className = 'badge-round'>
|
||||
<span>
|
||||
{ this.props._count || null }
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code ChatCounter}'s
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _count: number
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_count: getUnreadCount(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(ChatCounter);
|
|
@ -0,0 +1 @@
|
|||
export ChatCounter from './ChatCounter';
|
|
@ -0,0 +1,20 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* Selector for calculating the number of unread chat messages.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {number} The number of unread messages.
|
||||
*/
|
||||
export function getUnreadCount(state: Object) {
|
||||
const { lastReadMessage, messages } = state['features/chat'];
|
||||
const messagesCount = messages.length;
|
||||
|
||||
if (!messagesCount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const lastReadIndex = messages.lastIndexOf(lastReadMessage);
|
||||
|
||||
return messagesCount - (lastReadIndex + 1);
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
export * from './constants';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
SET_LAST_READ_MESSAGE
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
open: false,
|
||||
messages: [],
|
||||
lastReadMessage: null
|
||||
};
|
||||
|
||||
ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case ADD_MESSAGE: {
|
||||
const newMessage = {
|
||||
message: action.message,
|
||||
timestamp: action.timestamp,
|
||||
userName: action.userName
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
lastReadMessage:
|
||||
action.hasRead ? newMessage : state.lastReadMessage,
|
||||
messages: [
|
||||
...state.messages,
|
||||
newMessage
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
case SET_LAST_READ_MESSAGE:
|
||||
return {
|
||||
...state,
|
||||
lastReadMessage: action.message
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
|
@ -11,7 +11,14 @@ import { CalleeInfoContainer } from '../../base/jwt';
|
|||
import { Filmstrip } from '../../filmstrip';
|
||||
import { LargeVideo } from '../../large-video';
|
||||
import { NotificationsContainer } from '../../notifications';
|
||||
import { showToolbox, Toolbox } from '../../toolbox';
|
||||
import { SidePanel } from '../../side-panel';
|
||||
import {
|
||||
Toolbox,
|
||||
ToolboxV2,
|
||||
fullScreenChanged,
|
||||
setToolboxAlwaysVisible,
|
||||
showToolbox
|
||||
} from '../../toolbox';
|
||||
import { HideNotificationBarStyle } from '../../unsupported-browser';
|
||||
|
||||
import { maybeShowSuboptimalExperienceNotification } from '../functions';
|
||||
|
@ -19,11 +26,29 @@ import { maybeShowSuboptimalExperienceNotification } from '../functions';
|
|||
declare var APP: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* DOM events for when full screen mode has changed. Different browsers need
|
||||
* different vendor prefixes.
|
||||
*
|
||||
* @private
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
const FULL_SCREEN_EVENTS = [
|
||||
'webkitfullscreenchange',
|
||||
'mozfullscreenchange',
|
||||
'fullscreenchange'
|
||||
];
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Conference}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether the toolbar should stay visible or be able to autohide.
|
||||
*/
|
||||
_alwaysVisibleToolbar: boolean,
|
||||
|
||||
/**
|
||||
* Whether the local participant is recording the conference.
|
||||
*/
|
||||
|
@ -37,6 +62,7 @@ type Props = {
|
|||
* The conference page of the Web application.
|
||||
*/
|
||||
class Conference extends Component<Props> {
|
||||
_onFullScreenChange: Function;
|
||||
_onShowToolbar: Function;
|
||||
_originalOnShowToolbar: Function;
|
||||
|
||||
|
@ -59,6 +85,9 @@ class Conference extends Component<Props> {
|
|||
leading: true,
|
||||
trailing: false
|
||||
});
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onFullScreenChange = this._onFullScreenChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,10 +103,16 @@ class Conference extends Component<Props> {
|
|||
APP.UI.registerListeners();
|
||||
APP.UI.bindEvents();
|
||||
|
||||
const { dispatch, t } = this.props;
|
||||
FULL_SCREEN_EVENTS.forEach(name =>
|
||||
document.addEventListener(name, this._onFullScreenChange));
|
||||
|
||||
const { _alwaysVisibleToolbar, dispatch, t } = this.props;
|
||||
|
||||
dispatch(connect());
|
||||
maybeShowSuboptimalExperienceNotification(dispatch, t);
|
||||
|
||||
dispatch(setToolboxAlwaysVisible(
|
||||
_alwaysVisibleToolbar || interfaceConfig.filmStripOnly));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,6 +125,9 @@ class Conference extends Component<Props> {
|
|||
APP.UI.unregisterListeners();
|
||||
APP.UI.unbindEvents();
|
||||
|
||||
FULL_SCREEN_EVENTS.forEach(name =>
|
||||
document.removeEventListener(name, this._onFullScreenChange));
|
||||
|
||||
APP.conference.isJoined() && this.props.dispatch(disconnect());
|
||||
}
|
||||
|
||||
|
@ -100,12 +138,26 @@ class Conference extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { filmStripOnly, VIDEO_QUALITY_LABEL_DISABLED } = interfaceConfig;
|
||||
const {
|
||||
_USE_NEW_TOOLBOX,
|
||||
VIDEO_QUALITY_LABEL_DISABLED,
|
||||
filmStripOnly
|
||||
} = interfaceConfig;
|
||||
const hideVideoQualityLabel
|
||||
= filmStripOnly
|
||||
|| VIDEO_QUALITY_LABEL_DISABLED
|
||||
|| this.props._iAmRecorder;
|
||||
|
||||
let ToolboxToUse;
|
||||
|
||||
if (filmStripOnly) {
|
||||
ToolboxToUse = null;
|
||||
} else if (interfaceConfig._USE_NEW_TOOLBOX) {
|
||||
ToolboxToUse = ToolboxV2;
|
||||
} else {
|
||||
ToolboxToUse = Toolbox;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id = 'videoconference_page'
|
||||
|
@ -116,7 +168,10 @@ class Conference extends Component<Props> {
|
|||
<Filmstrip filmstripOnly = { filmStripOnly } />
|
||||
</div>
|
||||
|
||||
{ filmStripOnly ? null : <Toolbox /> }
|
||||
{ ToolboxToUse && <ToolboxToUse /> }
|
||||
|
||||
{ _USE_NEW_TOOLBOX && !filmStripOnly
|
||||
&& <SidePanel /> }
|
||||
|
||||
<DialogContainer />
|
||||
<NotificationsContainer />
|
||||
|
@ -135,6 +190,17 @@ class Conference extends Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Redux state when full screen mode has been enabled or
|
||||
* disabled.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFullScreenChange() {
|
||||
this.props.dispatch(fullScreenChanged(APP.UI.isFullScreen()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the toolbar.
|
||||
*
|
||||
|
@ -153,17 +219,30 @@ class Conference extends Component<Props> {
|
|||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _alwaysVisibleToolbar: boolean,
|
||||
* _iAmRecorder: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const {
|
||||
alwaysVisibleToolbar,
|
||||
iAmRecorder
|
||||
} = state['features/base/config'];
|
||||
|
||||
return {
|
||||
/**
|
||||
* Whether the toolbar should stay visible or be able to autohide.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_alwaysVisibleToolbar: alwaysVisibleToolbar,
|
||||
|
||||
/**
|
||||
* Whether the local participant is recording the conference.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_iAmRecorder: state['features/base/config'].iAmRecorder
|
||||
_iAmRecorder: iAmRecorder
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* The type of the action which signals document editing has been enabled.
|
||||
*
|
||||
* {
|
||||
* type: ETHERPAD_INITIALIZED
|
||||
* }
|
||||
*/
|
||||
export const ETHERPAD_INITIALIZED = Symbol('ETHERPAD_INITIALIZED');
|
||||
|
||||
|
||||
/**
|
||||
* The type of the action which signals document editing has stopped or started.
|
||||
*
|
||||
* {
|
||||
* type: SET_DOCUMENT_EDITING_STATUS
|
||||
* }
|
||||
*/
|
||||
export const SET_DOCUMENT_EDITING_STATUS
|
||||
= Symbol('SET_DOCUMENT_EDITING_STATUS');
|
||||
|
||||
/**
|
||||
* The type of the action which signals to start or stop editing a shared
|
||||
* document.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_DOCUMENT_EDITING
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_DOCUMENT_EDITING = Symbol('TOGGLE_DOCUMENT_EDITING');
|
|
@ -0,0 +1,50 @@
|
|||
// @flow
|
||||
|
||||
import {
|
||||
ETHERPAD_INITIALIZED,
|
||||
SET_DOCUMENT_EDITING_STATUS,
|
||||
TOGGLE_DOCUMENT_EDITING
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Dispatches an action to set whether document editing has started or stopped.
|
||||
*
|
||||
* @param {boolean} editing - Whether or not a document is currently being
|
||||
* edited.
|
||||
* @returns {{
|
||||
* type: SET_DOCUMENT_EDITING_STATUS,
|
||||
* editing: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setDocumentEditingState(editing: boolean) {
|
||||
return {
|
||||
type: SET_DOCUMENT_EDITING_STATUS,
|
||||
editing
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to set Etherpad as having been initialized.
|
||||
*
|
||||
* @returns {{
|
||||
* type: ETHERPAD_INITIALIZED
|
||||
* }}
|
||||
*/
|
||||
export function setEtherpadHasInitialzied() {
|
||||
return {
|
||||
type: ETHERPAD_INITIALIZED
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to show or hide Etherpad.
|
||||
*
|
||||
* @returns {{
|
||||
* type: TOGGLE_DOCUMENT_EDITING
|
||||
* }}
|
||||
*/
|
||||
export function toggleDocument() {
|
||||
return {
|
||||
type: TOGGLE_DOCUMENT_EDITING
|
||||
};
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
|
@ -0,0 +1,30 @@
|
|||
// @flow
|
||||
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
import { TOGGLE_DOCUMENT_EDITING } from './actionTypes';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Middleware that captures actions related to collaborative document editing
|
||||
* and notifies components not hooked into redux.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
if (typeof APP === 'undefined') {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case TOGGLE_DOCUMENT_EDITING:
|
||||
APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED);
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
ETHERPAD_INITIALIZED,
|
||||
SET_DOCUMENT_EDITING_STATUS
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/etherpad.
|
||||
*/
|
||||
ReducerRegistry.register('features/etherpad', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case ETHERPAD_INITIALIZED:
|
||||
return {
|
||||
...state,
|
||||
initialized: true
|
||||
};
|
||||
|
||||
case SET_DOCUMENT_EDITING_STATUS:
|
||||
return {
|
||||
...state,
|
||||
editing: action.editing
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
|
@ -7,11 +7,13 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
|
||||
import { InviteButton } from '../../invite';
|
||||
import { Toolbox } from '../../toolbox';
|
||||
import { Toolbox, ToolboxFilmstrip, dockToolbox } from '../../toolbox';
|
||||
|
||||
import { setFilmstripHovered } from '../actions';
|
||||
import { shouldRemoteVideosBeVisible } from '../functions';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which represents the filmstrip on
|
||||
* Web/React.
|
||||
|
@ -62,6 +64,12 @@ class Filmstrip extends Component<*> {
|
|||
*/
|
||||
_remoteVideosVisible: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Whether or not the toolbox is visible. The height of the vertical
|
||||
* filmstrip needs to adjust to accommodate the horizontal toolbox.
|
||||
*/
|
||||
_toolboxVisible: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Updates the redux store with filmstrip hover changes.
|
||||
*/
|
||||
|
@ -111,6 +119,7 @@ class Filmstrip extends Component<*> {
|
|||
_isAddToCallAvailable,
|
||||
_isDialOutAvailable,
|
||||
_remoteVideosVisible,
|
||||
_toolboxVisible,
|
||||
filmstripOnly
|
||||
} = this.props;
|
||||
|
||||
|
@ -122,13 +131,17 @@ class Filmstrip extends Component<*> {
|
|||
* will get updated without replacing the DOM. If the known DOM gets
|
||||
* modified, then the views will get blown away.
|
||||
*/
|
||||
const reduceHeight
|
||||
= _toolboxVisible && interfaceConfig.TOOLBAR_BUTTONS.length;
|
||||
const filmstripClassNames = `filmstrip ${_remoteVideosVisible
|
||||
? '' : 'hide-videos'} ${reduceHeight ? 'reduce-height' : ''}`;
|
||||
|
||||
const filmstripClassNames = `filmstrip ${_remoteVideosVisible ? ''
|
||||
: 'hide-videos'}`;
|
||||
const ToolboxToUse = interfaceConfig._USE_NEW_TOOLBOX
|
||||
? ToolboxFilmstrip : Toolbox;
|
||||
|
||||
return (
|
||||
<div className = { filmstripClassNames }>
|
||||
{ filmstripOnly ? <Toolbox /> : null }
|
||||
{ filmstripOnly ? <ToolboxToUse /> : null }
|
||||
<div
|
||||
className = 'filmstrip__videos'
|
||||
id = 'remoteVideos'>
|
||||
|
@ -172,6 +185,9 @@ class Filmstrip extends Component<*> {
|
|||
*/
|
||||
_notifyOfHoveredStateUpdate() {
|
||||
if (this.props._hovered !== this._isHovered) {
|
||||
if (interfaceConfig._USE_NEW_TOOLBOX) {
|
||||
this.props.dispatch(dockToolbox(this._isHovered));
|
||||
}
|
||||
this.props.dispatch(setFilmstripHovered(this._isHovered));
|
||||
}
|
||||
}
|
||||
|
@ -211,7 +227,8 @@ class Filmstrip extends Component<*> {
|
|||
* _hovered: boolean,
|
||||
* _isAddToCallAvailable: boolean,
|
||||
* _isDialOutAvailable: boolean,
|
||||
* _remoteVideosVisible: boolean
|
||||
* _remoteVideosVisible: boolean,
|
||||
* _toolboxVisible: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
|
@ -231,11 +248,13 @@ function _mapStateToProps(state) {
|
|||
|
||||
return {
|
||||
_hideInviteButton: iAmRecorder
|
||||
|| (!isAddToCallAvailable && !isDialOutAvailable),
|
||||
|| (!isAddToCallAvailable && !isDialOutAvailable)
|
||||
|| interfaceConfig._USE_NEW_TOOLBOX,
|
||||
_hovered: hovered,
|
||||
_isAddToCallAvailable: isAddToCallAvailable,
|
||||
_isDialOutAvailable: isDialOutAvailable,
|
||||
_remoteVideosVisible: shouldRemoteVideosBeVisible(state)
|
||||
_remoteVideosVisible: shouldRemoteVideosBeVisible(state),
|
||||
_toolboxVisible: state['features/toolbox'].visible
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,15 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ToolbarButton, TOOLTIP_TO_POPUP_POSITION } from '../../toolbox';
|
||||
import { createToolbarEvent, sendAnalytics } from '../../analytics';
|
||||
import { translate } from '../../base/i18n';
|
||||
import {
|
||||
ToolbarButton,
|
||||
ToolbarButtonV2,
|
||||
TOOLTIP_TO_POPUP_POSITION
|
||||
} from '../../toolbox';
|
||||
|
||||
import { setInfoDialogVisibility } from '../actions';
|
||||
import { setInfoDialogVisibility, updateDialInNumbers } from '../actions';
|
||||
import { InfoDialog } from './info-dialog';
|
||||
|
||||
const { INITIAL_TOOLBAR_TIMEOUT } = interfaceConfig;
|
||||
|
@ -39,6 +45,15 @@ class InfoDialogButton extends Component {
|
|||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* Phone numbers for dialing into the conference.
|
||||
*/
|
||||
_dialInNumbers: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.array
|
||||
]),
|
||||
|
||||
/**
|
||||
* Whether or not the {@code InfoDialog} should close by itself after a
|
||||
* a timeout.
|
||||
|
@ -61,6 +76,11 @@ class InfoDialogButton extends Component {
|
|||
*/
|
||||
dispatch: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func,
|
||||
|
||||
/**
|
||||
* From which side tooltips should display. Will be re-used for
|
||||
* displaying the inline dialog for video quality adjustment.
|
||||
|
@ -100,6 +120,10 @@ class InfoDialogButton extends Component {
|
|||
if (this.props._shouldAutoClose) {
|
||||
this._setAutoCloseTimeout();
|
||||
}
|
||||
|
||||
if (!this.props._dialInNumbers) {
|
||||
this.props.dispatch(updateDialInNumbers());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -145,29 +169,9 @@ class InfoDialogButton extends Component {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _showDialog, _toolboxVisible, tooltipPosition } = this.props;
|
||||
const buttonConfiguration = {
|
||||
...DEFAULT_BUTTON_CONFIGURATION,
|
||||
classNames: [
|
||||
...DEFAULT_BUTTON_CONFIGURATION.classNames,
|
||||
_showDialog ? 'toggled button-active' : ''
|
||||
]
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineDialog
|
||||
content = { <InfoDialog
|
||||
onClose = { this._onDialogClose }
|
||||
onMouseOver = { this._onDialogMouseOver } /> }
|
||||
isOpen = { _toolboxVisible && _showDialog }
|
||||
onClose = { this._onDialogClose }
|
||||
position = { TOOLTIP_TO_POPUP_POSITION[tooltipPosition] }>
|
||||
<ToolbarButton
|
||||
button = { buttonConfiguration }
|
||||
onClick = { this._onDialogToggle }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
</InlineDialog>
|
||||
);
|
||||
return interfaceConfig._USE_NEW_TOOLBOX
|
||||
? this._renderNewToolbarButton()
|
||||
: this._renderOldToolbarButton();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -208,9 +212,75 @@ class InfoDialogButton extends Component {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onDialogToggle() {
|
||||
sendAnalytics(createToolbarEvent('info'));
|
||||
|
||||
this.props.dispatch(setInfoDialogVisibility(!this.props._showDialog));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a React Element for the {@code InfoDialog} using legacy
|
||||
* {@code ToolbarButton}.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderOldToolbarButton() {
|
||||
const { _showDialog, _toolboxVisible, tooltipPosition } = this.props;
|
||||
const buttonConfiguration = {
|
||||
...DEFAULT_BUTTON_CONFIGURATION,
|
||||
classNames: [
|
||||
...DEFAULT_BUTTON_CONFIGURATION.classNames,
|
||||
_showDialog ? 'toggled button-active' : ''
|
||||
]
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineDialog
|
||||
content = { <InfoDialog
|
||||
autoUpdateNumbers = { false }
|
||||
onClose = { this._onDialogClose }
|
||||
onMouseOver = { this._onDialogMouseOver } /> }
|
||||
isOpen = { _toolboxVisible && _showDialog }
|
||||
onClose = { this._onDialogClose }
|
||||
position = { TOOLTIP_TO_POPUP_POSITION[tooltipPosition] }>
|
||||
<ToolbarButton
|
||||
button = { buttonConfiguration }
|
||||
onClick = { this._onDialogToggle }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
</InlineDialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a React Element for the {@code InfoDialog} using the newer
|
||||
* {@code ToolbarButtonV2}.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderNewToolbarButton() {
|
||||
const { _showDialog, _toolboxVisible, t } = this.props;
|
||||
const iconClass = `icon-info ${_showDialog ? 'toggled' : ''}`;
|
||||
|
||||
return (
|
||||
<div className = 'toolbox-button-wth-dialog'>
|
||||
<InlineDialog
|
||||
content = { <InfoDialog
|
||||
autoUpdateNumbers = { false }
|
||||
onClose = { this._onDialogClose }
|
||||
onMouseOver = { this._onDialogMouseOver } /> }
|
||||
isOpen = { _toolboxVisible && _showDialog }
|
||||
onClose = { this._onDialogClose }
|
||||
position = { 'top right' }>
|
||||
<ToolbarButtonV2
|
||||
iconName = { iconClass }
|
||||
onClick = { this._onDialogToggle }
|
||||
tooltip = { t('info.tooltip') } />
|
||||
</InlineDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a timeout to automatically hide the {@code InfoDialog}.
|
||||
*
|
||||
|
@ -235,6 +305,7 @@ class InfoDialogButton extends Component {
|
|||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _dialInNumbers: Array,
|
||||
* _shouldAutoClose: boolean,
|
||||
* _showDialog: boolean,
|
||||
* _toolboxVisible: boolean
|
||||
|
@ -243,14 +314,16 @@ class InfoDialogButton extends Component {
|
|||
function _mapStateToProps(state) {
|
||||
const {
|
||||
infoDialogVisible,
|
||||
infoDialogWillAutoClose
|
||||
infoDialogWillAutoClose,
|
||||
numbers
|
||||
} = state['features/invite'];
|
||||
|
||||
return {
|
||||
_dialInNumbers: numbers,
|
||||
_shouldAutoClose: infoDialogWillAutoClose,
|
||||
_showDialog: infoDialogVisible,
|
||||
_toolboxVisible: state['features/toolbox'].visible
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(InfoDialogButton);
|
||||
export default translate(connect(_mapStateToProps)(InfoDialogButton));
|
||||
|
|
|
@ -24,6 +24,15 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
|
|||
* @extends Component
|
||||
*/
|
||||
class InfoDialog extends Component {
|
||||
/**
|
||||
* Default values for {@code InfoDialog} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
autoUpdateNumbers: true
|
||||
};
|
||||
|
||||
/**
|
||||
* {@code InfoDialog} component's property types.
|
||||
*
|
||||
|
@ -69,6 +78,13 @@ class InfoDialog extends Component {
|
|||
*/
|
||||
_password: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Whether or not this component should make a request for dial-in
|
||||
* numbers. If false, this component will rely on an outside source
|
||||
* updating and passing in numbers through the _dialIn prop.
|
||||
*/
|
||||
autoUpdateNumbers: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Invoked to open a dialog for adding participants to the conference.
|
||||
*/
|
||||
|
@ -148,7 +164,7 @@ class InfoDialog extends Component {
|
|||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (!this.state.phoneNumber) {
|
||||
if (!this.state.phoneNumber && this.props.autoUpdateNumbers) {
|
||||
this.props.dispatch(updateDialInNumbers());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* The type of the action which signals the keyboard shortcuts dialog should
|
||||
* be displayed.
|
||||
*
|
||||
* {
|
||||
* type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
|
||||
* }
|
||||
*/
|
||||
export const OPEN_KEYBOARD_SHORTCUTS_DIALOG
|
||||
= Symbol('OPEN_KEYBOARD_SHORTCUTS_DIALOG');
|
|
@ -0,0 +1,14 @@
|
|||
import { OPEN_KEYBOARD_SHORTCUTS_DIALOG } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Opens the dialog showing available keyboard shortcuts.
|
||||
*
|
||||
* @returns {{
|
||||
* type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
|
||||
* }}
|
||||
*/
|
||||
export function openKeyboardShortcutsDialog() {
|
||||
return {
|
||||
type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
|
||||
};
|
||||
}
|
|
@ -1 +1,4 @@
|
|||
export * from './actions';
|
||||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import { OPEN_KEYBOARD_SHORTCUTS_DIALOG } from './actionTypes';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature keyboard-shortcuts.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case OPEN_KEYBOARD_SHORTCUTS_DIALOG:
|
||||
if (typeof APP === 'object') {
|
||||
APP.keyboardshortcut.openDialog();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
|
@ -20,3 +20,25 @@ export const HIDE_RECORDING_LABEL = Symbol('HIDE_RECORDING_LABEL');
|
|||
* @public
|
||||
*/
|
||||
export const RECORDING_STATE_UPDATED = Symbol('RECORDING_STATE_UPDATED');
|
||||
|
||||
/**
|
||||
* The type of Redux action which updates the current known type of configured
|
||||
* recording. For example, type "jibri" is used for live streaming.
|
||||
*
|
||||
* {
|
||||
* type: RECORDING_STATE_UPDATED,
|
||||
* recordingType: string
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const SET_RECORDING_TYPE = Symbol('SET_RECORDING_TYPE');
|
||||
|
||||
/**
|
||||
* The type of Redux action triggers the flow to start or stop recording.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_RECORDING
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const TOGGLE_RECORDING = Symbol('TOGGLE_RECORDING');
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { HIDE_RECORDING_LABEL, RECORDING_STATE_UPDATED } from './actionTypes';
|
||||
import {
|
||||
HIDE_RECORDING_LABEL,
|
||||
RECORDING_STATE_UPDATED,
|
||||
SET_RECORDING_TYPE,
|
||||
TOGGLE_RECORDING
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Hides any displayed recording label, regardless of current recording state.
|
||||
|
@ -13,6 +18,36 @@ export function hideRecordingLabel() {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets what type of recording service will be used.
|
||||
*
|
||||
* @param {string} recordingType - The type of recording service to be used.
|
||||
* Should be one of the enumerated types in {@link RECORDING_TYPES}.
|
||||
* @returns {{
|
||||
* type: SET_RECORDING_TYPE,
|
||||
* recordingType: string
|
||||
* }}
|
||||
*/
|
||||
export function setRecordingType(recordingType) {
|
||||
return {
|
||||
type: SET_RECORDING_TYPE,
|
||||
recordingType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start or stop recording.
|
||||
*
|
||||
* @returns {{
|
||||
* type: TOGGLE_RECORDING
|
||||
* }}
|
||||
*/
|
||||
export function toggleRecording() {
|
||||
return {
|
||||
type: TOGGLE_RECORDING
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the redux state for the recording feature.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* Expected supported recording types. JIBRI is known to support live streaming
|
||||
* whereas JIRECON is for recording.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
export const RECORDING_TYPES = {
|
||||
JIBRI: 'jibri',
|
||||
JIRECON: 'jirecon'
|
||||
};
|
|
@ -1,4 +1,6 @@
|
|||
export * from './actions';
|
||||
export * from './components';
|
||||
export * from './constants';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
// @flow
|
||||
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
import { TOGGLE_RECORDING } from './actionTypes';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature recording.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case TOGGLE_RECORDING:
|
||||
if (typeof APP === 'object') {
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_RECORDING);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
|
@ -1,5 +1,9 @@
|
|||
import { ReducerRegistry } from '../base/redux';
|
||||
import { HIDE_RECORDING_LABEL, RECORDING_STATE_UPDATED } from './actionTypes';
|
||||
import {
|
||||
HIDE_RECORDING_LABEL,
|
||||
RECORDING_STATE_UPDATED,
|
||||
SET_RECORDING_TYPE
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/recording.
|
||||
|
@ -18,6 +22,12 @@ ReducerRegistry.register('features/recording', (state = {}, action) => {
|
|||
...action.recordingState
|
||||
};
|
||||
|
||||
case SET_RECORDING_TYPE:
|
||||
return {
|
||||
...state,
|
||||
recordingType: action.recordingType
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* The type of the action which signals to update the current known state of the
|
||||
* shared YouTube video.
|
||||
*
|
||||
* {
|
||||
* type: SET_SHARED_VIDEO_STATUS,
|
||||
* status: string
|
||||
* }
|
||||
*/
|
||||
export const SET_SHARED_VIDEO_STATUS = Symbol('SET_SHARED_VIDEO_STATUS');
|
||||
|
||||
/**
|
||||
* The type of the action which signals to start the flow for starting or
|
||||
* stopping a shared YouTube video.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_SHARED_VIDEO
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_SHARED_VIDEO = Symbol('TOGGLE_SHARED_VIDEO');
|
|
@ -0,0 +1,31 @@
|
|||
import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Updates the current known status of the shared YouTube video.
|
||||
*
|
||||
* @param {string} status - The current status of the YouTube video being
|
||||
* shared.
|
||||
* @returns {{
|
||||
* type: SET_SHARED_VIDEO_STATUS,
|
||||
* status: string
|
||||
* }}
|
||||
*/
|
||||
export function setSharedVideoStatus(status) {
|
||||
return {
|
||||
type: SET_SHARED_VIDEO_STATUS,
|
||||
status
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the flow for starting or stopping a shared YouTube video.
|
||||
*
|
||||
* @returns {{
|
||||
* type: TOGGLE_SHARED_VIDEO
|
||||
* }}
|
||||
*/
|
||||
export function toggleSharedVideo() {
|
||||
return {
|
||||
type: TOGGLE_SHARED_VIDEO
|
||||
};
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
|
@ -0,0 +1,31 @@
|
|||
// @flow
|
||||
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import { TOGGLE_SHARED_VIDEO } from './actionTypes';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Middleware that captures actions related to YouTube video sharing and updates
|
||||
* components not hooked into redux.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
if (typeof APP === 'undefined') {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case TOGGLE_SHARED_VIDEO:
|
||||
APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/shared-video.
|
||||
*/
|
||||
ReducerRegistry.register('features/shared-video', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case SET_SHARED_VIDEO_STATUS:
|
||||
return {
|
||||
...state,
|
||||
status: action.status
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* The type of the action which signals to close the side panel.
|
||||
*
|
||||
* {
|
||||
* type: CLOSE_PANEL,
|
||||
* }
|
||||
*/
|
||||
export const CLOSE_PANEL = Symbol('CLOSE_PANEL');
|
||||
|
||||
/**
|
||||
* The type of the action which to set the name of the current panel being
|
||||
* displayed in the side panel.
|
||||
*
|
||||
* {
|
||||
* type: SET_VISIBLE_PANEL,
|
||||
* current: string|null
|
||||
* }
|
||||
*/
|
||||
export const SET_VISIBLE_PANEL = Symbol('SET_VISIBLE_PANEL');
|
||||
|
||||
/**
|
||||
* The type of the action which signals to toggle the display of chat in the
|
||||
* side panel.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_CHAT
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_CHAT = Symbol('TOGGLE_CHAT');
|
||||
|
||||
/**
|
||||
* The type of the action which signals to toggle the display of profile editing
|
||||
* in the side panel.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_PROFILE
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_PROFILE = Symbol('TOGGLE_PROFILE');
|
||||
|
||||
/**
|
||||
* The type of the action which signals to toggle the display of settings in the
|
||||
* side panel.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_SETTINGS
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_SETTINGS = Symbol('TOGGLE_SETTINGS');
|
|
@ -0,0 +1,77 @@
|
|||
import {
|
||||
CLOSE_PANEL,
|
||||
SET_VISIBLE_PANEL,
|
||||
TOGGLE_CHAT,
|
||||
TOGGLE_PROFILE,
|
||||
TOGGLE_SETTINGS
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Dispatches an action to close the currently displayed side panel.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function closePanel() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: CLOSE_PANEL,
|
||||
current: getState()['features/side-panel'].current
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the redux store with the currently displayed side panel.
|
||||
*
|
||||
* @param {string|null} name - The name of the side panel being displayed. Null
|
||||
* (or falsy) should be set if no side panel is being displayed.
|
||||
* @returns {{
|
||||
* type: SET_VISIBLE_PANEL,
|
||||
* current: string
|
||||
* }}
|
||||
*/
|
||||
export function setVisiblePanel(name = null) {
|
||||
return {
|
||||
type: SET_VISIBLE_PANEL,
|
||||
current: name
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles display of the chat side panel.
|
||||
*
|
||||
* @returns {{
|
||||
* type: TOGGLE_CHAT
|
||||
* }}
|
||||
*/
|
||||
export function toggleChat() {
|
||||
return {
|
||||
type: TOGGLE_CHAT
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles display of the profile side panel.
|
||||
*
|
||||
* @returns {{
|
||||
* type: TOGGLE_PROFILE
|
||||
* }}
|
||||
*/
|
||||
export function toggleProfile() {
|
||||
return {
|
||||
type: TOGGLE_PROFILE
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles display of the settings side panel.
|
||||
*
|
||||
* @returns {{
|
||||
* type: TOGGLE_SETTINGS
|
||||
* }}
|
||||
*/
|
||||
export function toggleSettings() {
|
||||
return {
|
||||
type: TOGGLE_SETTINGS
|
||||
};
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { closePanel } from '../actions';
|
||||
|
||||
/**
|
||||
* React Component for holding features in a side panel that slides in and out.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class SidePanel extends Component {
|
||||
/**
|
||||
* {@code SidePanel} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code SidePanel} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onCloseClick = this._onCloseClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<div id = 'sideToolbarContainer'>
|
||||
<div
|
||||
className = 'side-toolbar-close'
|
||||
onClick = { this._onCloseClick }>
|
||||
X
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to hide {@code SidePanel}.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCloseClick() {
|
||||
this.props.dispatch(closePanel());
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(SidePanel);
|
|
@ -0,0 +1 @@
|
|||
export { default as SidePanel } from './SidePanel';
|
|
@ -0,0 +1,6 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
|
@ -0,0 +1,45 @@
|
|||
// @flow
|
||||
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
CLOSE_PANEL,
|
||||
TOGGLE_CHAT,
|
||||
TOGGLE_PROFILE,
|
||||
TOGGLE_SETTINGS
|
||||
} from './actionTypes';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Middleware that catches actions related to the non-reactified web side panel.
|
||||
*
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
if (typeof APP !== 'object') {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case CLOSE_PANEL:
|
||||
APP.UI.toggleSidePanel(action.current);
|
||||
break;
|
||||
|
||||
case TOGGLE_CHAT:
|
||||
APP.UI.toggleChat();
|
||||
break;
|
||||
|
||||
case TOGGLE_PROFILE:
|
||||
APP.UI.toggleSidePanel('profile_container');
|
||||
break;
|
||||
|
||||
case TOGGLE_SETTINGS:
|
||||
APP.UI.toggleSidePanel('settings_container');
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_VISIBLE_PANEL } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/side-panel.
|
||||
*/
|
||||
ReducerRegistry.register('features/side-panel', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case SET_VISIBLE_PANEL:
|
||||
return {
|
||||
...state,
|
||||
current: action.current
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
|
@ -7,6 +7,17 @@
|
|||
*/
|
||||
export const CLEAR_TOOLBOX_TIMEOUT = Symbol('CLEAR_TOOLBOX_TIMEOUT');
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates whether the conference is or is not
|
||||
* currently in full screen view.
|
||||
*
|
||||
* {
|
||||
* type: FULL_SCREEN_CHANGED,
|
||||
* fullScreen: boolean
|
||||
* }
|
||||
*/
|
||||
export const FULL_SCREEN_CHANGED = Symbol('FULL_SCREEN_CHANGED');
|
||||
|
||||
/**
|
||||
* The type of the action which sets the default toolbar buttons of the Toolbox.
|
||||
*
|
||||
|
@ -19,6 +30,17 @@ export const CLEAR_TOOLBOX_TIMEOUT = Symbol('CLEAR_TOOLBOX_TIMEOUT');
|
|||
export const SET_DEFAULT_TOOLBOX_BUTTONS
|
||||
= Symbol('SET_DEFAULT_TOOLBOX_BUTTONS');
|
||||
|
||||
/**
|
||||
* The type of (redux) action which requests full screen mode be entered or
|
||||
* exited.
|
||||
*
|
||||
* {
|
||||
* type: SET_FULL_SCREEN,
|
||||
* fullScreen: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_FULL_SCREEN = Symbol('SET_FULL_SCREEN');
|
||||
|
||||
/**
|
||||
* The type of the action which sets the conference subject.
|
||||
*
|
||||
|
|
|
@ -229,9 +229,10 @@ export function toggleFullScreen(isFullScreen: boolean): Function {
|
|||
const buttonName = 'fullscreen';
|
||||
const button = getButton(buttonName, getState());
|
||||
|
||||
if (button) {
|
||||
button.toggled = isFullScreen;
|
||||
|
||||
dispatch(setToolbarButton(buttonName, button));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -14,10 +14,13 @@ import {
|
|||
setToolboxTimeout,
|
||||
setToolboxTimeoutMS,
|
||||
setToolboxVisible,
|
||||
toggleFullScreen,
|
||||
toggleToolbarButton
|
||||
} from './actions.native';
|
||||
import { SET_DEFAULT_TOOLBOX_BUTTONS } from './actionTypes';
|
||||
import {
|
||||
FULL_SCREEN_CHANGED,
|
||||
SET_DEFAULT_TOOLBOX_BUTTONS,
|
||||
SET_FULL_SCREEN
|
||||
} from './actionTypes';
|
||||
import {
|
||||
getButton,
|
||||
getDefaultToolboxButtons,
|
||||
|
@ -95,6 +98,23 @@ export function dockToolbox(dock: boolean): Function {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that full screen mode has been entered or exited.
|
||||
*
|
||||
* @param {boolean} fullScreen - Whether or not full screen mode is currently
|
||||
* enabled.
|
||||
* @returns {{
|
||||
* type: FULL_SCREEN_CHANGED,
|
||||
* fullScreen: boolean
|
||||
* }}
|
||||
*/
|
||||
export function fullScreenChanged(fullScreen: boolean) {
|
||||
return {
|
||||
type: FULL_SCREEN_CHANGED,
|
||||
fullScreen
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns button on mount/unmount handlers with dispatch function stored in
|
||||
* closure.
|
||||
|
@ -106,8 +126,6 @@ export function dockToolbox(dock: boolean): Function {
|
|||
function _getButtonHandlers(dispatch) {
|
||||
const localRaiseHandHandler
|
||||
= (...args) => dispatch(changeLocalRaiseHand(...args));
|
||||
const toggleFullScreenHandler
|
||||
= (...args) => dispatch(toggleFullScreen(...args));
|
||||
|
||||
return {
|
||||
/**
|
||||
|
@ -119,22 +137,6 @@ function _getButtonHandlers(dispatch) {
|
|||
onMount: () => dispatch(showDesktopSharingButton())
|
||||
},
|
||||
|
||||
/**
|
||||
* Mount/Unmount handler for toggling fullscreen button.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
fullscreen: {
|
||||
onMount: () =>
|
||||
APP.UI.addListener(
|
||||
UIEvents.FULLSCREEN_TOGGLED,
|
||||
toggleFullScreenHandler),
|
||||
onUnmount: () =>
|
||||
APP.UI.removeListener(
|
||||
UIEvents.FULLSCREEN_TOGGLED,
|
||||
toggleFullScreenHandler)
|
||||
},
|
||||
|
||||
/**
|
||||
* Mount/Unmount handlers for raisehand button.
|
||||
*
|
||||
|
@ -291,6 +293,22 @@ export function showDialPadButton(show: boolean): Function {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals a request to enter or exit full screen mode.
|
||||
*
|
||||
* @param {boolean} fullScreen - True to enter full screen mode, false to exit.
|
||||
* @returns {{
|
||||
* type: SET_FULL_SCREEN,
|
||||
* fullScreen: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setFullScreen(fullScreen: boolean) {
|
||||
return {
|
||||
type: SET_FULL_SCREEN,
|
||||
fullScreen
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows recording button.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import InlineDialog from '@atlaskit/inline-dialog';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
import ToolbarButtonV2 from './ToolbarButtonV2';
|
||||
|
||||
/**
|
||||
* A React {@code Component} for opening or closing the {@code OverflowMenu}.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class OverflowMenuButton extends Component {
|
||||
/**
|
||||
* {@code OverflowMenuButton} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* A child React Element to display within {@code InlineDialog}.
|
||||
*/
|
||||
children: PropTypes.object,
|
||||
|
||||
/**
|
||||
* Whether or not the OverflowMenu popover should display.
|
||||
*/
|
||||
isOpen: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Calback to change the visiblility of the overflow menu.
|
||||
*/
|
||||
onVisibilityChange: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code OverflowMenuButton} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCloseDialog = this._onCloseDialog.bind(this);
|
||||
this._onToggleDialogVisibility
|
||||
= this._onToggleDialogVisibility.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { children, isOpen, t } = this.props;
|
||||
const iconClasses = `icon-thumb-menu ${isOpen ? 'toggled' : ''}`;
|
||||
|
||||
return (
|
||||
<div className = 'toolbox-button-wth-dialog'>
|
||||
<InlineDialog
|
||||
content = { children }
|
||||
isOpen = { isOpen }
|
||||
onClose = { this._onCloseDialog }
|
||||
position = { 'top right' }>
|
||||
<ToolbarButtonV2
|
||||
iconName = { iconClasses }
|
||||
onClick = { this._onToggleDialogVisibility }
|
||||
tooltip = { t('toolbar.moreActions') } />
|
||||
</InlineDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when {@code InlineDialog} signals that it should be
|
||||
* close.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCloseDialog() {
|
||||
this.props.onVisibilityChange(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to signal that an event has occurred that should change
|
||||
* the visibility of the {@code InlineDialog} component.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleDialogVisibility() {
|
||||
this.props.onVisibilityChange(!this.props.isOpen);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(OverflowMenuButton);
|
|
@ -0,0 +1,53 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
/**
|
||||
* A React {@code Component} for displaying a link to interact with other
|
||||
* features of the application.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class OverflowMenuItem extends Component {
|
||||
/**
|
||||
* {@code OverflowMenuItem} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The icon class to use for displaying an icon before the link text.
|
||||
*/
|
||||
icon: PropTypes.string,
|
||||
|
||||
/**
|
||||
* The callback to invoke when {@code OverflowMenuItem} is clicked.
|
||||
*/
|
||||
onClick: PropTypes.func,
|
||||
|
||||
/**
|
||||
* The text to display in the {@code OverflowMenuItem}.
|
||||
*/
|
||||
text: PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<li
|
||||
className = 'overflow-menu-item'
|
||||
onClick = { this.props.onClick }>
|
||||
<span className = 'overflow-menu-item-icon'>
|
||||
<i className = { this.props.icon } />
|
||||
</span>
|
||||
{ this.props.text }
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OverflowMenuItem;
|
|
@ -0,0 +1,119 @@
|
|||
/* globals interfaceConfig */
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
getAvatarURL,
|
||||
getLocalParticipant
|
||||
} from '../../base/participants';
|
||||
|
||||
/**
|
||||
* A React {@code Component} for displaying a link with a profile avatar as an
|
||||
* icon.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class OverflowMenuProfileItem extends Component {
|
||||
/**
|
||||
* {@code OverflowMenuProfileItem}'s property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The redux representation of the local participant.
|
||||
*/
|
||||
_localParticipant: PropTypes.object,
|
||||
|
||||
/**
|
||||
* Whether the button support clicking or not.
|
||||
*/
|
||||
_unclickable: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* The callback to invoke when {@code OverflowMenuProfileItem} is
|
||||
* clicked.
|
||||
*/
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code OverflowMenuProfileItem} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _localParticipant, _unclickable } = this.props;
|
||||
const classNames = `overflow-menu-item ${
|
||||
_unclickable ? 'unclickable' : ''}`;
|
||||
const avatarURL = getAvatarURL(_localParticipant);
|
||||
let displayName;
|
||||
|
||||
if (_localParticipant && _localParticipant.name) {
|
||||
displayName = _localParticipant.name.split(' ')[0];
|
||||
} else {
|
||||
displayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className = { classNames }
|
||||
onClick = { this._onClick }>
|
||||
<span className = 'overflow-menu-item-icon'>
|
||||
<Avatar uri = { avatarURL } />
|
||||
</span>
|
||||
<span className = 'profile-text'>
|
||||
{ displayName }
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes an on click callback if clicking is allowed.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
if (!this.props._unclickable) {
|
||||
this.props.onClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code OverflowMenuProfileItem} component's props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _localParticipant: Object,
|
||||
* _unclickable: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_localParticipant: getLocalParticipant(state),
|
||||
_unclickable: !state['features/base/jwt'].isGuest
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(OverflowMenuProfileItem);
|
|
@ -0,0 +1,78 @@
|
|||
import Tooltip from '@atlaskit/tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import AbstractToolbarButton from './AbstractToolbarButton';
|
||||
|
||||
/**
|
||||
* Represents a button in the toolbar.
|
||||
*
|
||||
* @extends AbstractToolbarButton
|
||||
*/
|
||||
class ToolbarButtonV2 extends AbstractToolbarButton {
|
||||
/**
|
||||
* Default values for {@code ToolbarButtonV2} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
tooltipPosition: 'top'
|
||||
};
|
||||
|
||||
/**
|
||||
* {@code ToolbarButtonV2} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
...AbstractToolbarButton.propTypes,
|
||||
|
||||
/**
|
||||
* The text to display in the tooltip.
|
||||
*/
|
||||
tooltip: PropTypes.string,
|
||||
|
||||
/**
|
||||
* From which direction the tooltip should appear, relative to the
|
||||
* button.
|
||||
*/
|
||||
tooltipPosition: PropTypes.string
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the button of this {@code ToolbarButton}.
|
||||
*
|
||||
* @param {Object} children - The children, if any, to be rendered inside
|
||||
* the button. Presumably, contains the icon of this {@code ToolbarButton}.
|
||||
* @protected
|
||||
* @returns {ReactElement} The button of this {@code ToolbarButton}.
|
||||
*/
|
||||
_renderButton(children) {
|
||||
return (
|
||||
<div
|
||||
className = 'toolbox-button'
|
||||
onClick = { this.props.onClick }>
|
||||
<Tooltip
|
||||
description = { this.props.tooltip }
|
||||
position = { this.props.tooltipPosition }>
|
||||
{ children }
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the icon of this {@code ToolbarButton}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderIcon() {
|
||||
return (
|
||||
<div className = 'toolbox-icon'>
|
||||
<i className = { this.props.iconName } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ToolbarButtonV2;
|
|
@ -4,26 +4,16 @@ import React, { Component } from 'react';
|
|||
import { View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
AUDIO_MUTE,
|
||||
VIDEO_MUTE,
|
||||
createToolbarEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { toggleAudioOnly } from '../../base/conference';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
setAudioMuted,
|
||||
setVideoMuted,
|
||||
toggleCameraFacingMode,
|
||||
VIDEO_MUTISM_AUTHORITY
|
||||
toggleCameraFacingMode
|
||||
} from '../../base/media';
|
||||
import { Container } from '../../base/react';
|
||||
import {
|
||||
isNarrowAspectRatio,
|
||||
makeAspectRatioAware
|
||||
} from '../../base/responsive-ui';
|
||||
import { ColorPalette } from '../../base/styles';
|
||||
import {
|
||||
EnterPictureInPictureToolbarButton
|
||||
} from '../../mobile/picture-in-picture';
|
||||
|
@ -39,6 +29,8 @@ import AudioRouteButton from './AudioRouteButton';
|
|||
import styles from './styles';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
|
||||
import { AudioMuteButton, HangupButton, VideoMuteButton } from './buttons';
|
||||
|
||||
/**
|
||||
* The indicator which determines (at bundle time) whether there should be a
|
||||
* {@code ToolbarButton} in {@code Toolbox} to expose the functionality of the
|
||||
|
@ -118,20 +110,6 @@ type Props = {
|
|||
* Implements the conference toolbox on React Native.
|
||||
*/
|
||||
class Toolbox extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code Toolbox} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onToggleAudio = this._onToggleAudio.bind(this);
|
||||
this._onToggleVideo = this._onToggleVideo.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
|
@ -194,64 +172,6 @@ class Toolbox extends Component<Props> {
|
|||
};
|
||||
}
|
||||
|
||||
_onToggleAudio: () => void;
|
||||
|
||||
/**
|
||||
* Dispatches an action to toggle the mute state of the audio/microphone.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleAudio() {
|
||||
const mute = !this.props._audioMuted;
|
||||
|
||||
sendAnalytics(createToolbarEvent(
|
||||
AUDIO_MUTE,
|
||||
{
|
||||
enable: mute
|
||||
}));
|
||||
|
||||
// The user sees the reality i.e. the state of base/tracks and intends
|
||||
// to change reality by tapping on the respective button i.e. the user
|
||||
// sets the state of base/media. Whether the user's intention will turn
|
||||
// into reality is a whole different story which is of no concern to the
|
||||
// tapping.
|
||||
this.props.dispatch(
|
||||
setAudioMuted(
|
||||
mute,
|
||||
VIDEO_MUTISM_AUTHORITY.USER,
|
||||
/* ensureTrack */ true));
|
||||
}
|
||||
|
||||
_onToggleVideo: () => void;
|
||||
|
||||
/**
|
||||
* Dispatches an action to toggle the mute state of the video/camera.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleVideo() {
|
||||
const mute = !this.props._videoMuted;
|
||||
|
||||
sendAnalytics(createToolbarEvent(
|
||||
VIDEO_MUTE,
|
||||
{
|
||||
enable: mute
|
||||
}));
|
||||
|
||||
// The user sees the reality i.e. the state of base/tracks and intends
|
||||
// to change reality by tapping on the respective button i.e. the user
|
||||
// sets the state of base/media. Whether the user's intention will turn
|
||||
// into reality is a whole different story which is of no concern to the
|
||||
// tapping.
|
||||
this.props.dispatch(
|
||||
setVideoMuted(
|
||||
!this.props._videoMuted,
|
||||
VIDEO_MUTISM_AUTHORITY.USER,
|
||||
/* ensureTrack */ true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the toolbar which contains the primary buttons such as hangup,
|
||||
* audio and video mute.
|
||||
|
@ -269,24 +189,9 @@ class Toolbox extends Component<Props> {
|
|||
<View
|
||||
key = 'primaryToolbar'
|
||||
style = { styles.primaryToolbar }>
|
||||
<ToolbarButton
|
||||
iconName = { audioButtonStyles.iconName }
|
||||
iconStyle = { audioButtonStyles.iconStyle }
|
||||
onClick = { this._onToggleAudio }
|
||||
style = { audioButtonStyles.style } />
|
||||
<ToolbarButton
|
||||
accessibilityLabel = 'Hangup'
|
||||
iconName = 'hangup'
|
||||
iconStyle = { styles.whitePrimaryToolbarButtonIcon }
|
||||
onClick = { this.props._onHangup }
|
||||
style = { styles.hangup }
|
||||
underlayColor = { ColorPalette.buttonUnderlay } />
|
||||
<ToolbarButton
|
||||
disabled = { this.props._audioOnly }
|
||||
iconName = { videoButtonStyles.iconName }
|
||||
iconStyle = { videoButtonStyles.iconStyle }
|
||||
onClick = { this._onToggleVideo }
|
||||
style = { videoButtonStyles.style } />
|
||||
<AudioMuteButton buttonStyles = { audioButtonStyles } />
|
||||
<HangupButton />
|
||||
<VideoMuteButton buttonStyles = { videoButtonStyles } />
|
||||
</View>
|
||||
);
|
||||
|
||||
|
|
|
@ -5,8 +5,7 @@ import React, { Component } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
setDefaultToolboxButtons,
|
||||
setToolboxAlwaysVisible
|
||||
setDefaultToolboxButtons
|
||||
} from '../actions';
|
||||
import {
|
||||
abstractMapStateToProps
|
||||
|
@ -39,11 +38,6 @@ class Toolbox extends Component<*> {
|
|||
*/
|
||||
_setDefaultToolboxButtons: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Handler dispatching reset always visible toolbox action.
|
||||
*/
|
||||
_setToolboxAlwaysVisible: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Represents conference subject.
|
||||
*/
|
||||
|
@ -67,8 +61,6 @@ class Toolbox extends Component<*> {
|
|||
* @returns {void}
|
||||
*/
|
||||
componentDidMount(): void {
|
||||
this.props._setToolboxAlwaysVisible();
|
||||
|
||||
// FIXME The redux action SET_DEFAULT_TOOLBOX_BUTTONS and related source
|
||||
// code such as the redux action creator setDefaultToolboxButtons and
|
||||
// _setDefaultToolboxButtons were introduced to solve the following bug
|
||||
|
@ -168,8 +160,7 @@ class Toolbox extends Component<*> {
|
|||
*
|
||||
* @param {Function} dispatch - Redux action dispatcher.
|
||||
* @returns {{
|
||||
* _setDefaultToolboxButtons: Function,
|
||||
* _setToolboxAlwaysVisible: Function
|
||||
* _setDefaultToolboxButtons: Function
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
|
@ -182,18 +173,6 @@ function _mapDispatchToProps(dispatch: Function): Object {
|
|||
*/
|
||||
_setDefaultToolboxButtons() {
|
||||
dispatch(setDefaultToolboxButtons());
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatches a (redux) action to reset the permanent visibility of
|
||||
* the Toolbox.
|
||||
*
|
||||
* @returns {Object} Dispatched action.
|
||||
*/
|
||||
_setToolboxAlwaysVisible() {
|
||||
dispatch(setToolboxAlwaysVisible(
|
||||
config.alwaysVisibleToolbar === true
|
||||
|| interfaceConfig.filmStripOnly));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createToolbarEvent, sendAnalytics } from '../../analytics';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { openDeviceSelectionDialog } from '../../device-selection';
|
||||
|
||||
import ToolbarButtonV2 from './ToolbarButtonV2';
|
||||
import { AudioMuteButton, HangupButton, VideoMuteButton } from './buttons';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Implements the conference toolbox on React/Web for filmstrip only mode.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class ToolboxFilmstrip extends Component<*> {
|
||||
_visibleButtons: Object;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code ToolboxFilmstrip} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._visibleButtons = new Set(interfaceConfig.TOOLBAR_BUTTONS);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onToolbarOpenSettings = this._onToolbarOpenSettings.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'filmstrip-toolbox'>
|
||||
{ this._shouldShowButton('microphone')
|
||||
&& <AudioMuteButton tooltipPosition = 'left' /> }
|
||||
{ this._shouldShowButton('camera')
|
||||
&& <VideoMuteButton tooltipPosition = 'left' /> }
|
||||
{ this._shouldShowButton('fodeviceselection')
|
||||
&& <ToolbarButtonV2
|
||||
iconName = 'icon-settings'
|
||||
onClick = { this._onToolbarOpenSettings }
|
||||
tooltip = { t('toolbar.Settings') }
|
||||
tooltipPosition = 'left' /> }
|
||||
{ this._shouldShowButton('hangup')
|
||||
&& <HangupButton tooltipPosition = 'left' /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_onToolbarOpenSettings: () => void;
|
||||
|
||||
/**
|
||||
* Creates an analytics toolbar event for and dispatches an action to open
|
||||
* the device selection popup dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToolbarOpenSettings() {
|
||||
sendAnalytics(createToolbarEvent('filmstrip.only.device.selection'));
|
||||
|
||||
this.props.dispatch(openDeviceSelectionDialog());
|
||||
}
|
||||
|
||||
_shouldShowButton: (string) => boolean;
|
||||
|
||||
/**
|
||||
* Returns if a button name has been explicitly configured to be displayed.
|
||||
*
|
||||
* @param {string} buttonName - The name of the button, as expected in
|
||||
* {@link intefaceConfig}.
|
||||
* @private
|
||||
* @returns {boolean} True if the button should be displayed.
|
||||
*/
|
||||
_shouldShowButton(buttonName) {
|
||||
return this._visibleButtons.has(buttonName);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(ToolboxFilmstrip));
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,87 @@
|
|||
// @flow
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { Component } from 'react';
|
||||
|
||||
import {
|
||||
AUDIO_MUTE,
|
||||
createToolbarEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import {
|
||||
VIDEO_MUTISM_AUTHORITY,
|
||||
setAudioMuted
|
||||
} from '../../../base/media';
|
||||
|
||||
/**
|
||||
* An abstract implementation of a button for toggling audio mute.
|
||||
*/
|
||||
export default class AbstractAudioMuteButton extends Component<*> {
|
||||
/**
|
||||
* {@code AbstractAudioMuteButton} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Whether or not the local microphone is muted.
|
||||
*/
|
||||
_audioMuted: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Invoked to toggle audio mute.
|
||||
*/
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AbstractAudioMuteButton} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once per instance.
|
||||
this._onToolbarToggleAudio = this._onToolbarToggleAudio.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to toggle audio mute.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_doToggleAudio() {
|
||||
// The user sees the reality i.e. the state of base/tracks and intends
|
||||
// to change reality by tapping on the respective button i.e. the user
|
||||
// sets the state of base/media. Whether the user's intention will turn
|
||||
// into reality is a whole different story which is of no concern to the
|
||||
// tapping.
|
||||
this.props.dispatch(
|
||||
setAudioMuted(
|
||||
!this.props._audioMuted,
|
||||
VIDEO_MUTISM_AUTHORITY.USER,
|
||||
/* ensureTrack */ true));
|
||||
}
|
||||
|
||||
_onToolbarToggleAudio: () => void;
|
||||
|
||||
/**
|
||||
* Creates an analytics toolbar event and dispatches an action for toggling
|
||||
* audio mute.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToolbarToggleAudio() {
|
||||
sendAnalytics(createToolbarEvent(
|
||||
AUDIO_MUTE,
|
||||
{
|
||||
enable: !this.props._audioMuted
|
||||
}));
|
||||
|
||||
this._doToggleAudio();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
// @flow
|
||||
|
||||
import { Component } from 'react';
|
||||
|
||||
import {
|
||||
createToolbarEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
|
||||
/**
|
||||
* An abstract implementation of a button for leaving the conference.
|
||||
*/
|
||||
export default class AbstractHangupButton extends Component<*> {
|
||||
/**
|
||||
* Initializes a new {@code AbstractHangupButton} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once per instance.
|
||||
this._onToolbarHangup = this._onToolbarHangup.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action for leaving the current conference.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_doHangup() {
|
||||
/* to be implemented by descendants */
|
||||
}
|
||||
|
||||
_onToolbarHangup: () => void;
|
||||
|
||||
/**
|
||||
* Creates an analytics toolbar event and dispatches an action for leaving
|
||||
* the current conference.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToolbarHangup() {
|
||||
sendAnalytics(createToolbarEvent('hangup'));
|
||||
|
||||
this._doHangup();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
// @flow
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { Component } from 'react';
|
||||
|
||||
import {
|
||||
VIDEO_MUTE,
|
||||
createToolbarEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import {
|
||||
VIDEO_MUTISM_AUTHORITY,
|
||||
setVideoMuted
|
||||
} from '../../../base/media';
|
||||
|
||||
/**
|
||||
* An abstract implementation of a button for toggling video mute.
|
||||
*/
|
||||
export default class AbstractVideoMuteButton extends Component<*> {
|
||||
/**
|
||||
* {@code AbstractVideoMuteButton} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Whether or not the local camera is muted.
|
||||
*/
|
||||
_videoMuted: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Invoked to toggle video mute.
|
||||
*/
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AbstractVideoMuteButton} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once per instance.
|
||||
this._onToolbarToggleVideo = this._onToolbarToggleVideo.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to toggle the mute state of the video/camera.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_doToggleVideo() {
|
||||
// The user sees the reality i.e. the state of base/tracks and intends
|
||||
// to change reality by tapping on the respective button i.e. the user
|
||||
// sets the state of base/media. Whether the user's intention will turn
|
||||
// into reality is a whole different story which is of no concern to the
|
||||
// tapping.
|
||||
this.props.dispatch(
|
||||
setVideoMuted(
|
||||
!this.props._videoMuted,
|
||||
VIDEO_MUTISM_AUTHORITY.USER,
|
||||
/* ensureTrack */ true));
|
||||
}
|
||||
|
||||
|
||||
_onToolbarToggleVideo: () => void;
|
||||
|
||||
/**
|
||||
* Creates an analytics toolbar event and dispatches an action for toggling
|
||||
* video mute.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToolbarToggleVideo() {
|
||||
sendAnalytics(createToolbarEvent(
|
||||
VIDEO_MUTE,
|
||||
{
|
||||
enable: !this.props._videoMuted
|
||||
}));
|
||||
|
||||
this._doToggleVideo();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
// @flow
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import { isLocalTrackMuted } from '../../../base/tracks';
|
||||
|
||||
import AbstractAudioMuteButton from './AbstractAudioMuteButton';
|
||||
import ToolbarButton from '../ToolbarButton';
|
||||
|
||||
/**
|
||||
* Component that renders a toolbar button for toggling audio mute.
|
||||
*
|
||||
* @extends AbstractAudioMuteButton
|
||||
*/
|
||||
export class AudioMuteButton extends AbstractAudioMuteButton {
|
||||
/**
|
||||
* {@code AbstractAudioMuteButton} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
...AbstractAudioMuteButton.propTypes,
|
||||
|
||||
/**
|
||||
* Styles to be applied to the button and the icon to show.
|
||||
*/
|
||||
buttonStyles: PropTypes.object
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { buttonStyles } = this.props;
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
iconName = { buttonStyles.iconName }
|
||||
iconStyle = { buttonStyles.iconStyle }
|
||||
onClick = { this._onToolbarToggleAudio }
|
||||
style = { buttonStyles.style } />
|
||||
);
|
||||
}
|
||||
|
||||
_onToolbarToggleAudio: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code AudioMuteButton} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _audioMuted: boolean,
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const tracks = state['features/base/tracks'];
|
||||
|
||||
return {
|
||||
_audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default connect(_mapStateToProps)(AudioMuteButton);
|
|
@ -0,0 +1,166 @@
|
|||
// @flow
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
ACTION_SHORTCUT_TRIGGERED,
|
||||
AUDIO_MUTE,
|
||||
createShortcutEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import { isLocalTrackMuted } from '../../../base/tracks';
|
||||
|
||||
import AbstractAudioMuteButton from './AbstractAudioMuteButton';
|
||||
import ToolbarButtonV2 from '../ToolbarButtonV2';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Component that renders a toolbar button for toggling audio mute.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export class AudioMuteButton extends AbstractAudioMuteButton {
|
||||
/**
|
||||
* Default values for {@code AudioMuteButton} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
tooltipPosition: 'top'
|
||||
};
|
||||
|
||||
/**
|
||||
* {@code AudioMuteButton} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
...AbstractAudioMuteButton.propTypes,
|
||||
|
||||
/**
|
||||
* The {@code JitsiConference} for the current conference.
|
||||
*/
|
||||
_conference: PropTypes.object,
|
||||
|
||||
/**
|
||||
* Invoked to update the audio mute status.
|
||||
*/
|
||||
dispatch: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Where the tooltip should display, relative to the button.
|
||||
*/
|
||||
tooltipPosition: PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AudioMuteButton} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so it is only bound once per instance.
|
||||
this._onShortcutToggleAudio = this._onShortcutToggleAudio.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a keyboard shortcuts for toggling audio mute.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
APP.keyboardshortcut.registerShortcut(
|
||||
'M',
|
||||
null,
|
||||
this._onShortcutToggleAudio,
|
||||
'keyboardShortcuts.mute');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the registered keyboard shortcut handler.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
APP.keyboardshortcut.unregisterShortcut('M');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _audioMuted, _conference, t, tooltipPosition } = this.props;
|
||||
|
||||
return (
|
||||
<ToolbarButtonV2
|
||||
iconName = { _audioMuted && _conference
|
||||
? 'icon-mic-disabled toggled'
|
||||
: 'icon-microphone' }
|
||||
onClick = { this._onToolbarToggleAudio }
|
||||
tooltip = { t('toolbar.mute') }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
);
|
||||
}
|
||||
|
||||
_doToggleAudio: () => void;
|
||||
|
||||
_onShortcutToggleAudio: () => void;
|
||||
|
||||
/**
|
||||
* Creates an analytics keyboard shortcut event and dispatches an action for
|
||||
* toggling audio mute.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onShortcutToggleAudio() {
|
||||
sendAnalytics(createShortcutEvent(
|
||||
AUDIO_MUTE,
|
||||
ACTION_SHORTCUT_TRIGGERED,
|
||||
{ enable: !this.props._audioMuted }));
|
||||
|
||||
this._doToggleAudio();
|
||||
}
|
||||
|
||||
_onToolbarToggleAudio: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code AudioMuteButton} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _audioMuted: boolean,
|
||||
* _conference: Object,
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const tracks = state['features/base/tracks'];
|
||||
|
||||
return {
|
||||
_audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO),
|
||||
_conference: state['features/base/conference'].conference
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(AudioMuteButton));
|
|
@ -0,0 +1,63 @@
|
|||
// @flow
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { appNavigate } from '../../../app';
|
||||
import { ColorPalette } from '../../../base/styles';
|
||||
|
||||
import AbstractHangupButton from './AbstractHangupButton';
|
||||
import ToolbarButton from '../ToolbarButton';
|
||||
import styles from '../styles';
|
||||
|
||||
/**
|
||||
* Component that renders a toolbar button for leaving the current conference.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class HangupButton extends AbstractHangupButton {
|
||||
/**
|
||||
* {@code HangupButton} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Invoked to leave the conference.
|
||||
*/
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<ToolbarButton
|
||||
accessibilityLabel = 'Hangup'
|
||||
iconName = 'hangup'
|
||||
iconStyle = { styles.whitePrimaryToolbarButtonIcon }
|
||||
onClick = { this._onToolbarHangup }
|
||||
style = { styles.hangup }
|
||||
underlayColor = { ColorPalette.buttonUnderlay } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action for leaving the current conference.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_doHangup() {
|
||||
this.props.dispatch(appNavigate(undefined));
|
||||
}
|
||||
|
||||
_onToolbarHangup: () => void;
|
||||
}
|
||||
|
||||
export default connect()(HangupButton);
|
|
@ -0,0 +1,82 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { disconnect } from '../../../base/connection';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
import AbstractHangupButton from './AbstractHangupButton';
|
||||
import ToolbarButtonV2 from '../ToolbarButtonV2';
|
||||
|
||||
/**
|
||||
* Component that renders a toolbar button for leaving the current conference.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export class HangupButton extends AbstractHangupButton {
|
||||
/**
|
||||
* Default values for {@code HangupButton} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
tooltipPosition: 'top'
|
||||
};
|
||||
|
||||
/**
|
||||
* {@code HangupButton} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Invoked to trigger conference leave.
|
||||
*/
|
||||
dispatch: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Where the tooltip should display, relative to the button.
|
||||
*/
|
||||
tooltipPosition: PropTypes.string
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t, tooltipPosition } = this.props;
|
||||
|
||||
return (
|
||||
<ToolbarButtonV2
|
||||
iconName = 'icon-hangup'
|
||||
onClick = { this._onToolbarHangup }
|
||||
tooltip = { t('toolbar.hangup') }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
);
|
||||
}
|
||||
|
||||
_onToolbarHangup: () => void;
|
||||
|
||||
/**
|
||||
* Dispatches an action for leaving the current conference.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_doHangup() {
|
||||
this.props.dispatch(disconnect(true));
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(HangupButton));
|
|
@ -0,0 +1,82 @@
|
|||
// @flow
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import { isLocalTrackMuted } from '../../../base/tracks';
|
||||
|
||||
import AbstractVideoMuteButton from './AbstractVideoMuteButton';
|
||||
import ToolbarButton from '../ToolbarButton';
|
||||
|
||||
/**
|
||||
* Component that renders a toolbar button for toggling video mute.
|
||||
*
|
||||
* @extends AbstractVideoMuteButton
|
||||
*/
|
||||
class VideoMuteButton extends AbstractVideoMuteButton {
|
||||
/**
|
||||
* {@code VideoMuteButton} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
...AbstractVideoMuteButton.propTypes,
|
||||
|
||||
/**
|
||||
* Whether or not the local participant is current in audio only mode.
|
||||
* Video mute toggling is disabled in audio only mode.
|
||||
*/
|
||||
_audioOnly: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Styles to be applied to the button and the icon to show.
|
||||
*/
|
||||
buttonStyles: PropTypes.object
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _audioOnly, buttonStyles } = this.props;
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
disabled = { _audioOnly }
|
||||
iconName = { buttonStyles.iconName }
|
||||
iconStyle = { buttonStyles.iconStyle }
|
||||
onClick = { this._onToolbarToggleVideo }
|
||||
style = { buttonStyles.style } />
|
||||
);
|
||||
}
|
||||
|
||||
_onToolbarToggleVideo: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code VideoMuteButton} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _audioOnly: boolean,
|
||||
* _videoMuted: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const conference = state['features/base/conference'];
|
||||
const tracks = state['features/base/tracks'];
|
||||
|
||||
return {
|
||||
_audioOnly: Boolean(conference.audioOnly),
|
||||
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(VideoMuteButton);
|
|
@ -0,0 +1,161 @@
|
|||
// @flow
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
ACTION_SHORTCUT_TRIGGERED,
|
||||
VIDEO_MUTE,
|
||||
createShortcutEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import { isLocalTrackMuted } from '../../../base/tracks';
|
||||
|
||||
import AbstractVideoMuteButton from './AbstractVideoMuteButton';
|
||||
import ToolbarButtonV2 from '../ToolbarButtonV2';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Component that renders a toolbar button for toggling video mute.
|
||||
*
|
||||
* @extends AbstractVideoMuteButton
|
||||
*/
|
||||
export class VideoMuteButton extends AbstractVideoMuteButton {
|
||||
/**
|
||||
* Default values for {@code VideoMuteButton} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
tooltipPosition: 'top'
|
||||
};
|
||||
|
||||
/**
|
||||
* {@code VideoMuteButton} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
...AbstractVideoMuteButton.propTypes,
|
||||
|
||||
/**
|
||||
* The {@code JitsiConference} for the current conference.
|
||||
*/
|
||||
_conference: PropTypes.object,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Where the tooltip should display, relative to the button.
|
||||
*/
|
||||
tooltipPosition: PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code VideoMuteButton} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onShortcutToggleVideo = this._onShortcutToggleVideo.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a keyboard shortcuts for toggling video mute.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
APP.keyboardshortcut.registerShortcut(
|
||||
'V',
|
||||
null,
|
||||
this._onShortcutToggleVideo,
|
||||
'keyboardShortcuts.videoMute');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the registered keyboard shortcut handler.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
APP.keyboardshortcut.unregisterShortcut('V');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _conference, _videoMuted, t, tooltipPosition } = this.props;
|
||||
|
||||
return (
|
||||
<ToolbarButtonV2
|
||||
iconName = { _videoMuted && _conference
|
||||
? 'icon-camera-disabled toggled'
|
||||
: 'icon-camera' }
|
||||
onClick = { this._onToolbarToggleVideo }
|
||||
tooltip = { t('toolbar.videomute') }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
);
|
||||
}
|
||||
|
||||
_doToggleVideo: () => void;
|
||||
|
||||
_onShortcutToggleVideo: () => void;
|
||||
|
||||
/**
|
||||
* Creates an analytics keyboard shortcut event for and dispatches an action
|
||||
* for toggling video mute.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onShortcutToggleVideo() {
|
||||
sendAnalytics(createShortcutEvent(
|
||||
VIDEO_MUTE,
|
||||
ACTION_SHORTCUT_TRIGGERED,
|
||||
{ enable: !this.props._videoMuted }));
|
||||
|
||||
this._doToggleVideo();
|
||||
}
|
||||
|
||||
_onToolbarToggleVideo: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code AudioMuteButton} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: Object,
|
||||
* _videoMuted: boolean,
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const tracks = state['features/base/tracks'];
|
||||
|
||||
return {
|
||||
_conference: state['features/base/conference'].conference,
|
||||
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(VideoMuteButton));
|
|
@ -0,0 +1,3 @@
|
|||
export { default as AudioMuteButton } from './AudioMuteButton';
|
||||
export { default as HangupButton } from './HangupButton';
|
||||
export { default as VideoMuteButton } from './VideoMuteButton';
|
|
@ -1,4 +1,7 @@
|
|||
export { default as ToolbarButton } from './ToolbarButton';
|
||||
export { default as ToolbarButtonV2 } from './ToolbarButtonV2';
|
||||
export { default as ToolbarButtonWithDialog }
|
||||
from './ToolbarButtonWithDialog';
|
||||
export { default as Toolbox } from './Toolbox';
|
||||
export { default as ToolboxFilmstrip } from './ToolboxFilmstrip';
|
||||
export { default as ToolboxV2 } from './ToolboxV2';
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { setFullScreen } from '../toolbox';
|
||||
import {
|
||||
ACTION_SHORTCUT_TRIGGERED as TRIGGERED,
|
||||
AUDIO_MUTE,
|
||||
|
@ -10,6 +11,10 @@ import {
|
|||
createToolbarEvent,
|
||||
sendAnalytics
|
||||
} from '../analytics';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
participantUpdated
|
||||
} from '../base/participants';
|
||||
import { ParticipantCounter } from '../contact-list';
|
||||
import { openDeviceSelectionDialog } from '../device-selection';
|
||||
import { InfoDialogButton } from '../invite';
|
||||
|
@ -252,33 +257,37 @@ export default function getDefaultButtons() {
|
|||
enabled: true,
|
||||
id: 'toolbar_button_fullScreen',
|
||||
onClick() {
|
||||
// TODO: why is the fullscreen button handled differently than
|
||||
// the fullscreen keyboard shortcut (one results in a direct
|
||||
// call to toggleFullScreen, while the other fires an
|
||||
// UIEvents.TOGGLE_FULLSCREEN event)?
|
||||
const state = APP.store.getState();
|
||||
const isFullScreen = Boolean(
|
||||
state['features/toolbox'].fullScreen);
|
||||
|
||||
// The 'enable' attribute is set to true if the action resulted
|
||||
// in fullscreen mode being enabled.
|
||||
sendAnalytics(createToolbarEvent(
|
||||
'toggle.fullscreen',
|
||||
{
|
||||
enable: !APP.UI.isFullScreen()
|
||||
enable: !isFullScreen
|
||||
}));
|
||||
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_FULLSCREEN);
|
||||
APP.store.dispatch(setFullScreen(!isFullScreen));
|
||||
},
|
||||
shortcut: 'S',
|
||||
shortcutAttr: 'toggleFullscreenPopover',
|
||||
shortcutDescription: 'keyboardShortcuts.fullScreen',
|
||||
shortcutFunc() {
|
||||
const state = APP.store.getState();
|
||||
const isFullScreen = Boolean(
|
||||
state['features/toolbox'].fullScreen);
|
||||
|
||||
// The 'enable' attribute is set to true if the action resulted
|
||||
// in fullscreen mode being enabled.
|
||||
sendAnalytics(createShortcutEvent(
|
||||
'toggle.fullscreen',
|
||||
{
|
||||
enable: !APP.UI.isFullScreen()
|
||||
enable: !isFullScreen
|
||||
}));
|
||||
APP.UI.toggleFullScreen();
|
||||
|
||||
APP.store.dispatch(setFullScreen(!isFullScreen));
|
||||
},
|
||||
tooltipKey: 'toolbar.fullscreen'
|
||||
},
|
||||
|
@ -394,27 +403,44 @@ export default function getDefaultButtons() {
|
|||
id: 'toolbar_button_raisehand',
|
||||
onClick() {
|
||||
// TODO: reduce duplication with shortcutFunc below.
|
||||
const localParticipant
|
||||
= getLocalParticipant(APP.store.getState());
|
||||
const currentRaisedHand = localParticipant.raisedHand;
|
||||
|
||||
// The 'enable' attribute is set to true if the pressing of the
|
||||
// shortcut resulted in the hand being raised, and to false
|
||||
// if it resulted in the hand being 'lowered'.
|
||||
sendAnalytics(createToolbarEvent(
|
||||
'raise.hand',
|
||||
{ enable: !APP.conference.isHandRaised }));
|
||||
APP.conference.maybeToggleRaisedHand();
|
||||
{ enable: !currentRaisedHand }));
|
||||
|
||||
APP.store.dispatch(participantUpdated({
|
||||
id: localParticipant.id,
|
||||
local: true,
|
||||
raisedHand: !currentRaisedHand
|
||||
}));
|
||||
},
|
||||
shortcut: 'R',
|
||||
shortcutAttr: 'raiseHandPopover',
|
||||
shortcutDescription: 'keyboardShortcuts.raiseHand',
|
||||
shortcutFunc() {
|
||||
const localParticipant
|
||||
= getLocalParticipant(APP.store.getState());
|
||||
const currentRaisedHand = localParticipant.raisedHand;
|
||||
|
||||
// The 'enable' attribute is set to true if the pressing of the
|
||||
// shortcut resulted in the hand being raised, and to false
|
||||
// if it resulted in the hand being 'lowered'.
|
||||
sendAnalytics(createShortcutEvent(
|
||||
'toggle.raise.hand',
|
||||
TRIGGERED,
|
||||
{ enable: !APP.conference.isHandRaised }));
|
||||
APP.conference.maybeToggleRaisedHand();
|
||||
{ enable: !currentRaisedHand }));
|
||||
|
||||
APP.store.dispatch(participantUpdated({
|
||||
id: localParticipant.id,
|
||||
local: true,
|
||||
raisedHand: !currentRaisedHand
|
||||
}));
|
||||
},
|
||||
tooltipKey: 'toolbar.raiseHand'
|
||||
},
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue