feat(new-toolbars): initial implementation

This commit is contained in:
Leonard Kim 2018-03-06 16:28:19 -08:00 committed by Lyubo Marinov
parent 962df14382
commit d93782af8a
108 changed files with 5227 additions and 1164 deletions

View File

@ -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

View File

@ -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;

View File

@ -180,3 +180,6 @@
.icon-gsm-bars:before {
content: "\e926";
}
.icon-open_in_new:before {
content: "\e89e";
}

View File

@ -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%;

View File

@ -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 {

View File

@ -36,6 +36,8 @@ $alwaysOnTopToolbarFontSize: 1em;
$alwaysOnTopToolbarSize: 30px;
$defaultToolbarSize: 50px;
$defaultFilmStripOnlyToolbarSize: 37px;
$newToolbarSize: 50px;
$newToolbarFontSize: 1.9em;
$secToolbarFontSize: 1.9em;
$secToolbarLineHeight: 45px;
$toolbarAvatarPadding: 10px;

View File

@ -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.

View File

@ -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 {

Binary file not shown.

View File

@ -23,6 +23,7 @@
<glyph unicode="&#xe616;" 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="&#xe61d;" 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="&#xe80b;" 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="&#xe89e;" 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="&#xe8b3;" 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="&#xe901;" 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="&#xe902;" 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

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -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.

View File

@ -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",

View File

@ -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());
};

View File

@ -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));
}
}

View File

@ -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
}));

View File

@ -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();

View File

@ -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));
}
},

View File

@ -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);
chat.height(window.innerHeight - 15 - msgareaHeight);
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, '&gt;')
.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));
},
/**

View File

@ -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);
},
/**

View File

@ -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

View File

@ -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.

View File

@ -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.
*

View File

@ -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.
*

View File

@ -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);
}

View File

@ -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

View File

@ -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 {
@ -47,7 +52,7 @@ MiddlewareRegistry.register(store => next => action => {
const { conference } = store.getState()['features/base/conference'];
if (action.type === PARTICIPANT_JOINED
|| action.type === PARTICIPANT_LEFT) {
|| action.type === PARTICIPANT_LEFT) {
_maybePlaySounds(store, action);
}
@ -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);

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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');

View File

@ -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
};
}

View File

@ -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);

View File

@ -0,0 +1 @@
export ChatCounter from './ChatCounter';

View File

@ -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);
}

View File

@ -1,3 +1,7 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './constants';
import './middleware';
import './reducer';

View File

@ -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;
});

View File

@ -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
};
}

View File

@ -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');

View File

@ -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
};
}

View File

@ -0,0 +1,5 @@
export * from './actions';
export * from './actionTypes';
import './middleware';
import './reducer';

View File

@ -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);
});

View File

@ -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;
}
});

View File

@ -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
};
}

View File

@ -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));

View File

@ -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());
}
}

View File

@ -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');

View File

@ -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
};
}

View File

@ -1 +1,4 @@
export * from './actions';
export * from './components';
import './middleware';

View File

@ -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);
});

View File

@ -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');

View File

@ -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.
*

View File

@ -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'
};

View File

@ -1,4 +1,6 @@
export * from './actions';
export * from './components';
export * from './constants';
import './middleware';
import './reducer';

View File

@ -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);
});

View File

@ -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;
}

View File

@ -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');

View File

@ -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
};
}

View File

@ -0,0 +1,5 @@
export * from './actions';
export * from './actionTypes';
import './middleware';
import './reducer';

View File

@ -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);
});

View File

@ -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;
}
});

View File

@ -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');

View File

@ -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
};
}

View File

@ -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);

View File

@ -0,0 +1 @@
export { default as SidePanel } from './SidePanel';

View File

@ -0,0 +1,6 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
import './middleware';
import './reducer';

View File

@ -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);
});

View File

@ -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;
});

View File

@ -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.
*

View File

@ -229,9 +229,10 @@ export function toggleFullScreen(isFullScreen: boolean): Function {
const buttonName = 'fullscreen';
const button = getButton(buttonName, getState());
button.toggled = isFullScreen;
dispatch(setToolbarButton(buttonName, button));
if (button) {
button.toggled = isFullScreen;
dispatch(setToolbarButton(buttonName, button));
}
};
}

View File

@ -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.
*

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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>
);

View File

@ -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));
}
};
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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));

View File

@ -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);

View File

@ -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));

View File

@ -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);

View File

@ -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));

View File

@ -0,0 +1,3 @@
export { default as AudioMuteButton } from './AudioMuteButton';
export { default as HangupButton } from './HangupButton';
export { default as VideoMuteButton } from './VideoMuteButton';

View File

@ -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';

View File

@ -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