React Toolbar

This commit is contained in:
Ilya Daynatovich 2017-02-16 17:02:40 -06:00 committed by Lyubo Marinov
parent 0d7cb63978
commit da4425b5c0
30 changed files with 3246 additions and 1519 deletions

View File

@ -20,6 +20,8 @@ import analytics from './modules/analytics/analytics';
import EventEmitter from "events"; import EventEmitter from "events";
import { showDesktopSharingButton } from './react/features/toolbar';
import { import {
AVATAR_ID_COMMAND, AVATAR_ID_COMMAND,
AVATAR_URL_COMMAND, AVATAR_URL_COMMAND,
@ -583,6 +585,9 @@ export default {
APP.connection = connection = con; APP.connection = connection = con;
this.isDesktopSharingEnabled = this.isDesktopSharingEnabled =
JitsiMeetJS.isDesktopSharingEnabled(); JitsiMeetJS.isDesktopSharingEnabled();
APP.store.dispatch(showDesktopSharingButton());
APP.remoteControl.init(); APP.remoteControl.init();
this._createRoom(tracks); this._createRoom(tracks);

View File

@ -67,17 +67,3 @@
from { width: 0; } from { width: 0; }
to { width: $sidebarWidth; } to { width: $sidebarWidth; }
} }
/**
* Fade in / out animations
**/
@include keyframes(fadeIn) {
from { opacity: 0; }
to { opacity: 1; }
}
@include keyframes(fadeOut) {
from { opacity: 1; }
to { opacity: 0; }
}

View File

@ -1,8 +1,11 @@
.notice { .notice {
position: relative; position: absolute;
left: 50%;
z-index: $zindex3; z-index: $zindex3;
margin-top: 6px; margin-top: 6px;
@include transform(translateX(-50%));
&__message { &__message {
background-color: #000000; background-color: #000000;
color: white; color: white;

View File

@ -1,59 +1,138 @@
/**
* Round badge.
*/
.badge-round {
background-color: $toolbarBadgeBackground;
border-radius: 50%;
box-sizing: border-box;
color: $toolbarBadgeColor;
// Do not inherit the font-family from the toolbar button, because it's an
// icon style.
font-family: $baseFontFamily;
font-size: 9px;
font-weight: 700;
line-height: 13px;
min-width: 13px;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
vertical-align: middle;
}
/**
* Toolbar button styles.
*/
.button {
color: #FFFFFF;
cursor: pointer;
z-index: $zindex1;
display: inline-block;
font-size: $toolbarFontSize !important;
height: 50px;
line-height: 50px !important;
position: relative;
text-align: center;
top:0px;
vertical-align: middle;
width: 50px;
&_hangup {
color: $hangupColor;
font-size: $hangupFontSize !important;
}
&[disabled] {
opacity: 0.5;
}
&:hover, &:active {
cursor: pointer;
text-decoration: none;
}
&:not(.toggled) {
&:hover, &:active {
// sum opacity with background layer should give us 0.8
background: $toolbarSelectBackground;
}
}
&.toggled {
background: $toolbarToggleBackground;
&.icon-camera {
@extend .icon-camera-disabled;
}
&.icon-full-screen {
@extend .icon-exit-full-screen;
}
&.icon-microphone {
@extend .icon-mic-disabled;
}
}
&.unclickable {
cursor: default;
&:hover, &:active, &.selected {
background: none;
cursor: default;
}
}
}
.toolbar-container {
display: block;
left:0;
min-height: 100px;
opacity: 0;
pointer-events: none;
position: absolute;
right:0;
text-align: center;
top:0;
z-index: $toolbarZ;
}
/**
* Common toolbar styles.
*/
.toolbar { .toolbar {
background-color: $toolbarBackground; background-color: $toolbarBackground;
position: relative;
z-index: $toolbarZ;
height: 100%; height: 100%;
pointer-events: auto; pointer-events: auto;
position: relative;
z-index: $toolbarZ;
/** /**
* Splitter button in the toolbar. * Splitter button in the toolbar.
*/ */
&__splitter { &__splitter {
background: $splitterColor;
display: inline-block; display: inline-block;
vertical-align: middle;
width: 1px;
height: 50%; height: 50%;
margin: 0 $splitterToolbarButtonMargin; margin: 0 $splitterToolbarButtonMargin;
background: $splitterColor; vertical-align: middle;
} width: 1px;
} }
#mainToolbarContainer{ /**
display: block; * Primary toolbar styles.
*/
&_primary {
position: absolute; position: absolute;
text-align: center; left: 50%;
top:0;
left:0;
right:0;
z-index: $toolbarZ;
pointer-events: none;
min-height: 100px;
opacity: 0;
}
#subject {
position: relative;
z-index: $zindex3;
width: auto;
padding: 5px;
margin-left: 40%;
margin-right: 40%;
text-align: center;
background: linear-gradient(to bottom, rgba(255,255,255,.85) , rgba(255,255,255,.35));
box-shadow: 0 0 2px #000000, 0 0 10px #000000;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
#mainToolbar {
height: $defaultToolbarSize;
display: inline-block;
position: relative;
top: 30px; top: 30px;
margin-left: auto; display: inline-block;
margin-right: auto;
width: auto; width: auto;
height: $defaultToolbarSize;
border-radius: 3px; border-radius: 3px;
opacity: 0;
@include transform(translateX(-50%));
.button:first-child { .button:first-child {
border-bottom-left-radius: 3px; border-bottom-left-radius: 3px;
border-top-left-radius: 3px; border-top-left-radius: 3px;
@ -64,121 +143,92 @@
} }
} }
#extendedToolbar { &_primary a.button:last-child::after {
content: none;
}
/**
* Secondary toolbar styles.
*/
&_secondary {
position: absolute;
align-items: center;
box-sizing: border-box;
display: -moz-box; display: -moz-box;
display: -ms-flexbox; display: -ms-flexbox;
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
width: $defaultToolbarSize;
height: 100%;
top: 0;
left: 0;
padding-top: 10px;
box-sizing: border-box;
flex-direction: column; flex-direction: column;
flex-wrap: nowrap; flex-wrap: nowrap;
height: 100%;
justify-content: flex-start; justify-content: flex-start;
align-items: center; left: 0;
padding-top: 10px;
top: 0;
transform: translateX(-100%); transform: translateX(-100%);
width: $defaultToolbarSize;
-webkit-transform: translateX(-100%); -webkit-transform: translateX(-100%);
}
#toolbar_button_hangup { .button.toggled:not(.icon-raised-hand) {
color: #BF2117; background: $toolbarSelectBackground;
font-size: $hangupFontSize !important;
}
#toolbar_button_etherpad {
display: none;
}
#mainToolbar a.button:last-child::after {
content: none;
}
.button {
display: inline-block;
position: relative;
color: #FFFFFF;
top:0px;
width: 50px;
height: 50px;
cursor: pointer;
text-align: center;
z-index: $zindex1;
font-size: $toolbarFontSize !important;
line-height: 50px !important;
vertical-align: middle;
}
.button[disabled] {
opacity: 0.5;
}
.button.unclickable {
cursor: default;
}
.button.toggled {
background: $toolbarToggleBackground !important;
}
a.button.unclickable:hover,
a.button.unclickable:active,
a.button.unclickable.selected{
cursor: default;
background: none;
}
a.button:hover,
a.button:active,
a.button.selected {
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
// sum opacity with background layer should give us 0.8
background: $toolbarSelectBackground;
}
a.button>#avatar { &.unclickable {
width: 30px; cursor: default;
border-radius: 50%;
padding-top: 10px;
padding-bottom: 10px;
}
#feedbackButton { &:hover, &:active, &.selected {
margin-top: auto; background: none;
cursor: default;
}
}
} }
/**
* Round badge.
*/
.badge-round {
background-color: $toolbarBadgeBackground;
color: $toolbarBadgeColor;
font-size: 9px;
line-height: 13px;
font-weight: 700;
text-align: center;
border-radius: 50%;
min-width: 13px;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
vertical-align: middle;
// Do not inherit the font-family from the toolbar button, because it's an
// icon style.
font-family: $baseFontFamily;
} }
/** /**
* Toolbar specific round badge. * Toolbar specific round badge.
*/ */
.toolbar .badge-round { .badge-round {
bottom: 9px;
position: absolute; position: absolute;
right: 9px; right: 9px;
bottom: 9px; }
}
.subject {
background: linear-gradient(to bottom, rgba(255,255,255,.85) , rgba(255,255,255,.35));
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
box-shadow: 0 0 2px #000000, 0 0 10px #000000;
margin-left: 40%;
margin-right: 40%;
padding: 5px;
position: relative;
text-align: center;
width: auto;
z-index: $zindex3;
&.subject_slide-in {
top: 80px;
@include transition(top .3s ease-in);
}
&.subject_slide-out {
top: 0;
@include transition(top .3s ease-out);
}
}
a.button>#avatar {
border-radius: 50%;
padding-bottom: 10px;
padding-top: 10px;
width: 30px;
}
#feedbackButton {
margin-top: auto;
} }
/** /**
@ -272,9 +322,13 @@ a.button>#avatar {
* START of fade in animation for main toolbar * START of fade in animation for main toolbar
*/ */
.fadeIn { .fadeIn {
@include animation('fadeIn .3s linear .2s forwards'); opacity: 1;
@include transition(all .3s ease-in);
} }
.fadeOut { .fadeOut {
@include animation('fadeOut .5s linear forwards'); opacity: 0;
@include transition(all .3s ease-out);
} }

View File

@ -4,13 +4,12 @@
* Style variables * Style variables
*/ */
$baseFontFamily: 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif; $baseFontFamily: 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$toolbarFontSize: 1.9em; $hangupColor: #bf2117;
$hangupFontSize: 2em; $hangupFontSize: 2em;
/** /**
* Size variables. * Size variables.
*/ */
$defaultToolbarSize: 50px;
// Video layout. // Video layout.
$thumbnailToolbarHeight: 22px; $thumbnailToolbarHeight: 22px;
@ -34,14 +33,16 @@ $tooltipBg: rgba(0,0,0, 0.7);
/** /**
* Toolbar * Toolbar
*/ */
$toolbarTitleColor: #FFFFFF; $defaultToolbarSize: 50px;
$toolbarTitleFontSize: 19px; $splitterToolbarButtonMargin: 18px;
$toolbarBackground: rgba(0, 0, 0, 0.5); $toolbarBackground: rgba(0, 0, 0, 0.5);
$toolbarSelectBackground: rgba(0, 0, 0, .6);
$toolbarBadgeBackground: #165ECC; $toolbarBadgeBackground: #165ECC;
$toolbarBadgeColor: #FFFFFF; $toolbarBadgeColor: #FFFFFF;
$toolbarFontSize: 1.9em;
$toolbarSelectBackground: rgba(0, 0, 0, .6);
$toolbarTitleColor: #FFFFFF;
$toolbarTitleFontSize: 19px;
$toolbarToggleBackground: #12499C; $toolbarToggleBackground: #12499C;
$splitterToolbarButtonMargin: 18px;
/** /**
* Main controls * Main controls

View File

@ -6,8 +6,6 @@ var UI = {};
import Chat from "./side_pannels/chat/Chat"; import Chat from "./side_pannels/chat/Chat";
import SidePanels from "./side_pannels/SidePanels"; import SidePanels from "./side_pannels/SidePanels";
import Toolbar from "./toolbars/Toolbar";
import ToolbarToggler from "./toolbars/ToolbarToggler";
import Avatar from "./avatar/Avatar"; import Avatar from "./avatar/Avatar";
import SideContainerToggler from "./side_pannels/SideContainerToggler"; import SideContainerToggler from "./side_pannels/SideContainerToggler";
import UIUtil from "./util/UIUtil"; import UIUtil from "./util/UIUtil";
@ -25,6 +23,23 @@ import RingOverlay from "./ring_overlay/RingOverlay";
import UIErrors from './UIErrors'; import UIErrors from './UIErrors';
import { debounce } from "../util/helpers"; import { debounce } from "../util/helpers";
import {
setAudioMuted,
setVideoMuted
} from '../../react/features/base/media';
import {
checkAutoEnableDesktopSharing,
dockToolbar,
setAudioIconEnabled,
setToolbarButton,
setVideoIconEnabled,
showDialPadButton,
showEtherpadButton,
showSharedVideoButton,
showSIPCallButton,
showToolbar
} from '../../react/features/toolbar';
var EventEmitter = require("events"); var EventEmitter = require("events");
UI.messageHandler = require("./util/MessageHandler"); UI.messageHandler = require("./util/MessageHandler");
var messageHandler = UI.messageHandler; var messageHandler = UI.messageHandler;
@ -83,16 +98,6 @@ JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.CONSTRAINT_FAILED]
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.NO_DATA_FROM_SOURCE] JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.NO_DATA_FROM_SOURCE]
= "dialog.micNotSendingData"; = "dialog.micNotSendingData";
/**
* Initialize toolbars with side panels.
*/
function setupToolbars() {
// Initialize toolbar buttons
Toolbar.init(eventEmitter);
// Initialize side panels
SidePanels.init(eventEmitter);
}
/** /**
* Toggles the application in and out of full screen mode * Toggles the application in and out of full screen mode
* (a.k.a. presentation mode in Chrome). * (a.k.a. presentation mode in Chrome).
@ -231,7 +236,7 @@ UI.initConference = function () {
UI.setUserAvatarID(id, Settings.getAvatarId()); UI.setUserAvatarID(id, Settings.getAvatarId());
} }
Toolbar.checkAutoEnableDesktopSharing(); APP.store.dispatch(checkAutoEnableDesktopSharing());
if(!interfaceConfig.filmStripOnly) { if(!interfaceConfig.filmStripOnly) {
Feedback.init(eventEmitter); Feedback.init(eventEmitter);
@ -294,7 +299,6 @@ UI.start = function () {
// Set the defaults for tooltips. // Set the defaults for tooltips.
_setTooltipDefaults(); _setTooltipDefaults();
ToolbarToggler.init();
SideContainerToggler.init(eventEmitter); SideContainerToggler.init(eventEmitter);
FilmStrip.init(eventEmitter); FilmStrip.init(eventEmitter);
@ -313,11 +317,9 @@ UI.start = function () {
{ leading: true, trailing: false }); { leading: true, trailing: false });
$("#videoconference_page").mousemove(debouncedShowToolbar); $("#videoconference_page").mousemove(debouncedShowToolbar);
setupToolbars();
// Initialise the recording module. // Initialize side panels
if (config.enableRecording) SidePanels.init(eventEmitter);
Recording.init(eventEmitter, config.recordingType);
} else { } else {
$("body").addClass("filmstrip-only"); $("body").addClass("filmstrip-only");
UIUtil.setVisible('mainToolbarContainer', false); UIUtil.setVisible('mainToolbarContainer', false);
@ -446,7 +448,8 @@ UI.initEtherpad = name => {
logger.log('Etherpad is enabled'); logger.log('Etherpad is enabled');
etherpadManager etherpadManager
= new EtherpadManager(config.etherpad_base, name, eventEmitter); = new EtherpadManager(config.etherpad_base, name, eventEmitter);
Toolbar.showEtherpadButton();
APP.store.dispatch(showEtherpadButton());
}; };
/** /**
@ -521,8 +524,9 @@ UI.onPeerVideoTypeChanged
UI.updateLocalRole = isModerator => { UI.updateLocalRole = isModerator => {
VideoLayout.showModeratorIndicator(); VideoLayout.showModeratorIndicator();
Toolbar.showSipCallButton(isModerator); APP.store.dispatch(showSIPCallButton(isModerator));
Toolbar.showSharedVideoButton(isModerator); APP.store.dispatch(showSharedVideoButton());
Recording.showRecordingButton(isModerator); Recording.showRecordingButton(isModerator);
SettingsMenu.showStartMutedOptions(isModerator); SettingsMenu.showStartMutedOptions(isModerator);
SettingsMenu.showFollowMeOptions(isModerator); SettingsMenu.showFollowMeOptions(isModerator);
@ -676,7 +680,10 @@ UI.askForNickname = function () {
UI.setAudioMuted = function (id, muted) { UI.setAudioMuted = function (id, muted) {
VideoLayout.onAudioMute(id, muted); VideoLayout.onAudioMute(id, muted);
if (APP.conference.isLocalId(id)) { if (APP.conference.isLocalId(id)) {
Toolbar.toggleAudioIcon(muted); APP.store.dispatch(setAudioMuted(muted));
APP.store.dispatch(setToolbarButton('microphone', {
toggled: muted
}));
} }
}; };
@ -686,7 +693,10 @@ UI.setAudioMuted = function (id, muted) {
UI.setVideoMuted = function (id, muted) { UI.setVideoMuted = function (id, muted) {
VideoLayout.onVideoMute(id, muted); VideoLayout.onVideoMute(id, muted);
if (APP.conference.isLocalId(id)) { if (APP.conference.isLocalId(id)) {
Toolbar.toggleVideoIcon(muted); APP.store.dispatch(setVideoMuted(muted));
APP.store.dispatch(setToolbarButton('camera', {
toggled: muted
}));
} }
}; };
@ -716,7 +726,7 @@ UI.removeListener = function (type, listener) {
* @param type the type of the event we're emitting * @param type the type of the event we're emitting
* @param options the parameters for the event * @param options the parameters for the event
*/ */
UI.emitEvent = (type, options) => eventEmitter.emit(type, options); UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options);
UI.clickOnVideo = function (videoNumber) { UI.clickOnVideo = function (videoNumber) {
let videos = $("#remoteVideos .videocontainer:not(#mixedstream)"); let videos = $("#remoteVideos .videocontainer:not(#mixedstream)");
@ -731,12 +741,12 @@ UI.clickOnVideo = function (videoNumber) {
//Used by torture //Used by torture
UI.showToolbar = function (timeout) { UI.showToolbar = function (timeout) {
return ToolbarToggler.showToolbar(timeout); APP.store.dispatch(showToolbar(timeout));
}; };
//Used by torture //Used by torture
UI.dockToolbar = function (isDock) { UI.dockToolbar = function (isDock) {
ToolbarToggler.dockToolbar(isDock); APP.store.dispatch(dockToolbar(isDock));
}; };
/** /**
@ -916,10 +926,14 @@ UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
/** /**
* Update state of desktop sharing buttons. * Update state of desktop sharing buttons.
*
* @returns {void}
*/ */
UI.updateDesktopSharingButtons = function () { UI.updateDesktopSharingButtons
Toolbar.updateDesktopSharingButtonState(); = () =>
}; APP.store.dispatch(setToolbarButton('desktop', {
toggled: APP.conference.isSharingScreen
}));
/** /**
* Hide connection quality statistics from UI. * Hide connection quality statistics from UI.
@ -970,11 +984,8 @@ UI.addMessage = function (from, displayName, message, stamp) {
Chat.updateChatConversation(from, displayName, message, stamp); Chat.updateChatConversation(from, displayName, message, stamp);
}; };
// eslint-disable-next-line no-unused-vars UI.updateDTMFSupport
UI.updateDTMFSupport = function (isDTMFSupported) { = isDTMFSupported => APP.store.dispatch(showDialPadButton(isDTMFSupported));
//TODO: enable when the UI is ready
//Toolbar.showDialPadButton(isDTMFSupported);
};
/** /**
* Show user feedback dialog if its required and enabled after pressing the * Show user feedback dialog if its required and enabled after pressing the
@ -1315,7 +1326,8 @@ UI.onSharedVideoStop = function (id, attributes) {
* @param {boolean} enabled indicates if the camera button should be enabled * @param {boolean} enabled indicates if the camera button should be enabled
* or disabled * or disabled
*/ */
UI.setCameraButtonEnabled = enabled => Toolbar.setVideoIconEnabled(enabled); UI.setCameraButtonEnabled
= enabled => APP.store.dispatch(setVideoIconEnabled(enabled));
/** /**
* Enables / disables microphone toolbar button. * Enables / disables microphone toolbar button.
@ -1323,7 +1335,8 @@ UI.setCameraButtonEnabled = enabled => Toolbar.setVideoIconEnabled(enabled);
* @param {boolean} enabled indicates if the microphone button should be * @param {boolean} enabled indicates if the microphone button should be
* enabled or disabled * enabled or disabled
*/ */
UI.setMicrophoneButtonEnabled = enabled => Toolbar.setAudioIconEnabled(enabled); UI.setMicrophoneButtonEnabled
= enabled => APP.store.dispatch(setAudioIconEnabled(enabled));
UI.showRingOverlay = function () { UI.showRingOverlay = function () {
RingOverlay.show(APP.tokenData.callee, interfaceConfig.DISABLE_RINGING); RingOverlay.show(APP.tokenData.callee, interfaceConfig.DISABLE_RINGING);

View File

@ -20,7 +20,8 @@ import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from '../util/UIUtil'; import UIUtil from '../util/UIUtil';
import VideoLayout from '../videolayout/VideoLayout'; import VideoLayout from '../videolayout/VideoLayout';
import Feedback from '../feedback/Feedback.js'; import Feedback from '../feedback/Feedback.js';
import Toolbar from '../toolbars/Toolbar';
import { hideToolbar } from '../../../react/features/toolbar';
/** /**
* The dialog for user input. * The dialog for user input.
@ -263,7 +264,7 @@ var Recording = {
APP.conference.getMyUserId(), false); APP.conference.getMyUserId(), false);
VideoLayout.setLocalVideoVisible(false); VideoLayout.setLocalVideoVisible(false);
Feedback.enableFeedback(false); Feedback.enableFeedback(false);
Toolbar.enable(false); APP.store.dispatch(hideToolbar());
APP.UI.messageHandler.enableNotifications(false); APP.UI.messageHandler.enableNotifications(false);
APP.UI.messageHandler.enablePopups(false); APP.UI.messageHandler.enablePopups(false);
} }

View File

@ -9,7 +9,8 @@ import VideoLayout from "../videolayout/VideoLayout";
import LargeContainer from '../videolayout/LargeContainer'; import LargeContainer from '../videolayout/LargeContainer';
import SmallVideo from '../videolayout/SmallVideo'; import SmallVideo from '../videolayout/SmallVideo';
import FilmStrip from '../videolayout/FilmStrip'; import FilmStrip from '../videolayout/FilmStrip';
import ToolbarToggler from "../toolbars/ToolbarToggler";
import { dockToolbar, showToolbar } from '../../../react/features/toolbar';
export const SHARED_VIDEO_CONTAINER_TYPE = "sharedvideo"; export const SHARED_VIDEO_CONTAINER_TYPE = "sharedvideo";
@ -578,7 +579,7 @@ class SharedVideoContainer extends LargeContainer {
self.bodyBackground = document.body.style.background; self.bodyBackground = document.body.style.background;
document.body.style.background = 'black'; document.body.style.background = 'black';
this.$iframe.css({opacity: 1}); this.$iframe.css({opacity: 1});
ToolbarToggler.dockToolbar(true); APP.store.dispatch(dockToolbar(true));
resolve(); resolve();
}); });
}); });
@ -586,7 +587,7 @@ class SharedVideoContainer extends LargeContainer {
hide () { hide () {
let self = this; let self = this;
ToolbarToggler.dockToolbar(false); APP.store.dispatch(dockToolbar(false));
return new Promise(resolve => { return new Promise(resolve => {
this.$iframe.fadeOut(300, () => { this.$iframe.fadeOut(300, () => {
document.body.style.background = self.bodyBackground; document.body.style.background = self.bodyBackground;
@ -597,7 +598,7 @@ class SharedVideoContainer extends LargeContainer {
} }
onHoverIn () { onHoverIn () {
ToolbarToggler.showToolbar(); APP.store.dispatch(showToolbar());
} }
get id () { get id () {

View File

@ -2,7 +2,6 @@
import {processReplacements, linkify} from './Replacement'; import {processReplacements, linkify} from './Replacement';
import CommandsProcessor from './Commands'; import CommandsProcessor from './Commands';
import ToolbarToggler from '../../toolbars/ToolbarToggler';
import VideoLayout from "../../videolayout/VideoLayout"; import VideoLayout from "../../videolayout/VideoLayout";
import UIUtil from '../../util/UIUtil'; import UIUtil from '../../util/UIUtil';
@ -10,6 +9,8 @@ import UIEvents from '../../../../service/UI/UIEvents';
import { smileys } from './smileys'; import { smileys } from './smileys';
import { dockToolbar, setSubject } from '../../../../react/features/toolbar';
let unreadMessages = 0; let unreadMessages = 0;
const sidePanelsContainerId = 'sideToolbarContainer'; const sidePanelsContainerId = 'sideToolbarContainer';
const htmlStr = ` const htmlStr = `
@ -59,7 +60,7 @@ function updateVisualNotification() {
if (unreadMessages) { if (unreadMessages) {
unreadMsgElement.innerHTML = unreadMessages.toString(); unreadMsgElement.innerHTML = unreadMessages.toString();
ToolbarToggler.dockToolbar(true); APP.store.dispatch(dockToolbar(true));
const chatButtonElement const chatButtonElement
= document.getElementById('toolbar_button_chat'); = document.getElementById('toolbar_button_chat');
@ -238,7 +239,7 @@ var Chat = {
// Undock the toolbar when the chat is shown and if we're in a // Undock the toolbar when the chat is shown and if we're in a
// video mode. // video mode.
if (VideoLayout.isLargeVideoVisible()) { if (VideoLayout.isLargeVideoVisible()) {
ToolbarToggler.dockToolbar(false); APP.store.dispatch(dockToolbar(false));
} }
// if we are in conversation mode focus on the text input // if we are in conversation mode focus on the text input
@ -319,10 +320,9 @@ var Chat = {
subject = subject.trim(); subject = subject.trim();
} }
let subjectId = 'subject';
const html = linkify(UIUtil.escapeHtml(subject)); const html = linkify(UIUtil.escapeHtml(subject));
$(`#${subjectId}`).html(html);
UIUtil.setVisible(subjectId, subject && subject.length > 0); APP.store.dispatch(setSubject(html));
}, },
/** /**

View File

@ -1,889 +0,0 @@
/* global AJS, APP, $, config, interfaceConfig, JitsiMeetJS */
import UIUtil from '../util/UIUtil';
import UIEvents from '../../../service/UI/UIEvents';
import SideContainerToggler from "../side_pannels/SideContainerToggler";
let emitter = null;
let Toolbar;
/**
* Handlers for toolbar buttons.
*
* buttonId {string}: handler {function}
*/
const buttonHandlers = {
"toolbar_button_profile": function () {
JitsiMeetJS.analytics.sendEvent('toolbar.profile.toggled');
emitter.emit(UIEvents.TOGGLE_PROFILE);
},
"toolbar_button_mute": function () {
let sharedVideoManager = APP.UI.getSharedVideoManager();
if (APP.conference.audioMuted) {
// If there's a shared video with the volume "on" and we aren't
// the video owner, we warn the user
// that currently it's not possible to unmute.
if (sharedVideoManager
&& sharedVideoManager.isSharedVideoVolumeOn()
&& !sharedVideoManager.isSharedVideoOwner()) {
APP.UI.showCustomToolbarPopup(
'#unableToUnmutePopup', true, 5000);
}
else {
JitsiMeetJS.analytics.sendEvent('toolbar.audio.unmuted');
emitter.emit(UIEvents.AUDIO_MUTED, false, true);
}
} else {
JitsiMeetJS.analytics.sendEvent('toolbar.audio.muted');
emitter.emit(UIEvents.AUDIO_MUTED, true, true);
}
},
"toolbar_button_camera": function () {
if (APP.conference.videoMuted) {
JitsiMeetJS.analytics.sendEvent('toolbar.video.enabled');
emitter.emit(UIEvents.VIDEO_MUTED, false);
} else {
JitsiMeetJS.analytics.sendEvent('toolbar.video.disabled');
emitter.emit(UIEvents.VIDEO_MUTED, true);
}
},
"toolbar_button_link": function () {
JitsiMeetJS.analytics.sendEvent('toolbar.invite.clicked');
emitter.emit(UIEvents.INVITE_CLICKED);
},
"toolbar_button_chat": function () {
JitsiMeetJS.analytics.sendEvent('toolbar.chat.toggled');
emitter.emit(UIEvents.TOGGLE_CHAT);
},
"toolbar_contact_list": function () {
JitsiMeetJS.analytics.sendEvent(
'toolbar.contacts.toggled');
emitter.emit(UIEvents.TOGGLE_CONTACT_LIST);
},
"toolbar_button_etherpad": function () {
JitsiMeetJS.analytics.sendEvent('toolbar.etherpad.clicked');
emitter.emit(UIEvents.ETHERPAD_CLICKED);
},
"toolbar_button_sharedvideo": function () {
JitsiMeetJS.analytics.sendEvent('toolbar.sharedvideo.clicked');
emitter.emit(UIEvents.SHARED_VIDEO_CLICKED);
},
"toolbar_button_desktopsharing": function () {
if (APP.conference.isSharingScreen) {
JitsiMeetJS.analytics.sendEvent('toolbar.screen.disabled');
} else {
JitsiMeetJS.analytics.sendEvent('toolbar.screen.enabled');
}
emitter.emit(UIEvents.TOGGLE_SCREENSHARING);
},
"toolbar_button_fullScreen": function() {
JitsiMeetJS.analytics.sendEvent('toolbar.fullscreen.enabled');
emitter.emit(UIEvents.TOGGLE_FULLSCREEN);
},
"toolbar_button_sip": function () {
JitsiMeetJS.analytics.sendEvent('toolbar.sip.clicked');
showSipNumberInput();
},
"toolbar_button_dialpad": function () {
JitsiMeetJS.analytics.sendEvent('toolbar.sip.dialpad.clicked');
dialpadButtonClicked();
},
"toolbar_button_settings": function () {
JitsiMeetJS.analytics.sendEvent('toolbar.settings.toggled');
emitter.emit(UIEvents.TOGGLE_SETTINGS);
},
"toolbar_button_hangup": function () {
JitsiMeetJS.analytics.sendEvent('toolbar.hangup');
emitter.emit(UIEvents.HANGUP);
},
"toolbar_button_raisehand": function () {
JitsiMeetJS.analytics.sendEvent('toolbar.raiseHand.clicked');
APP.conference.maybeToggleRaisedHand();
}
};
/**
* All toolbars buttons description
*/
const defaultToolbarButtons = {
'microphone': {
id: 'toolbar_button_mute',
tooltipKey: 'toolbar.mute',
className: "button icon-microphone",
shortcut: 'M',
shortcutAttr: 'mutePopover',
shortcutFunc: function() {
JitsiMeetJS.analytics.sendEvent('shortcut.audiomute.toggled');
APP.conference.toggleAudioMuted();
},
shortcutDescription: "keyboardShortcuts.mute",
popups: [
{
id: 'micMutedPopup',
className: 'loginmenu',
dataAttr: '[title]toolbar.micMutedPopup'
},
{
id: 'unableToUnmutePopup',
className: 'loginmenu',
dataAttr: '[title]toolbar.unableToUnmutePopup'
},
{
id: 'talkWhileMutedPopup',
className: 'loginmenu',
dataAttr: '[title]toolbar.talkWhileMutedPopup'
}
],
content: "Mute / Unmute",
i18n: "[content]toolbar.mute"
},
'camera': {
id: 'toolbar_button_camera',
tooltipKey: 'toolbar.videomute',
className: "button icon-camera",
shortcut: 'V',
shortcutAttr: 'toggleVideoPopover',
shortcutFunc: function() {
JitsiMeetJS.analytics.sendEvent('shortcut.videomute.toggled');
APP.conference.toggleVideoMuted();
},
shortcutDescription: "keyboardShortcuts.videoMute",
content: "Start / stop camera",
i18n: "[content]toolbar.videomute"
},
'desktop': {
id: 'toolbar_button_desktopsharing',
tooltipKey: 'toolbar.sharescreen',
className: 'button icon-share-desktop',
shortcut: 'D',
shortcutAttr: 'toggleDesktopSharingPopover',
shortcutFunc: function() {
JitsiMeetJS.analytics.sendEvent('shortcut.screen.toggled');
APP.conference.toggleScreenSharing();
},
shortcutDescription: 'keyboardShortcuts.toggleScreensharing',
content: 'Share screen',
i18n: '[content]toolbar.sharescreen'
},
'invite': {
id: 'toolbar_button_link',
tooltipKey: 'toolbar.invite',
className: 'button icon-link',
content: 'Invite others',
i18n: '[content]toolbar.invite'
},
'chat': {
id: 'toolbar_button_chat',
tooltipKey: 'toolbar.chat',
className: 'button icon-chat',
shortcut: 'C',
shortcutAttr: 'toggleChatPopover',
shortcutFunc: function() {
JitsiMeetJS.analytics.sendEvent('shortcut.chat.toggled');
APP.UI.toggleChat();
},
shortcutDescription: 'keyboardShortcuts.toggleChat',
sideContainerId: 'chat_container',
html: `<span class="badge-round">
<span id="unreadMessages"></span>
</span>`
},
'contacts': {
id: 'toolbar_contact_list',
tooltipKey: 'bottomtoolbar.contactlist',
className: 'button icon-contactList',
sideContainerId: 'contacts_container',
html: `<span class="badge-round">
<span id="numberOfParticipants"></span>
</span>`
},
'profile': {
id: 'toolbar_button_profile',
tooltipKey: 'profile.setDisplayNameLabel',
className: 'button',
sideContainerId: 'profile_container',
html: `<img id="avatar" src="images/avatar2.png"/>`
},
'etherpad': {
id: 'toolbar_button_etherpad',
tooltipKey: 'toolbar.etherpad',
className: 'button icon-share-doc'
},
'fullscreen': {
id: 'toolbar_button_fullScreen',
tooltipKey: 'toolbar.fullscreen',
className: "button icon-full-screen",
shortcut: 'S',
shortcutAttr: 'toggleFullscreenPopover',
shortcutFunc: function() {
JitsiMeetJS.analytics.sendEvent('shortcut.fullscreen.toggled');
APP.UI.toggleFullScreen();
},
shortcutDescription: "keyboardShortcuts.fullScreen",
content: "Enter / Exit Full Screen",
i18n: "[content]toolbar.fullscreen"
},
'settings': {
id: 'toolbar_button_settings',
tooltipKey: 'toolbar.Settings',
className: 'button icon-settings',
sideContainerId: "settings_container"
},
'hangup': {
id: 'toolbar_button_hangup',
tooltipKey: 'toolbar.hangup',
className: "button icon-hangup",
content: "Hang Up",
i18n: "[content]toolbar.hangup"
},
'raisehand': {
id: "toolbar_button_raisehand",
tooltipKey: 'toolbar.raiseHand',
className: "button icon-raised-hand",
shortcut: "R",
shortcutAttr: "raiseHandPopover",
shortcutFunc: function() {
JitsiMeetJS.analytics.sendEvent("shortcut.raisehand.clicked");
APP.conference.maybeToggleRaisedHand();
},
shortcutDescription: "keyboardShortcuts.raiseHand",
content: "Raise Hand",
i18n: "[content]toolbar.raiseHand"
},
//init and btn handler: Recording.initRecordingButton (Recording.js)
'recording': {
id: 'toolbar_button_record',
tooltipKey: 'liveStreaming.buttonTooltip',
className: 'button',
hidden: true // will be displayed once
// the recording functionality is detected
},
'sharedvideo': {
id: 'toolbar_button_sharedvideo',
tooltipKey: 'toolbar.sharedvideo',
className: 'button icon-shared-video',
popups: [
{
id: 'sharedVideoMutedPopup',
className: 'loginmenu extendedToolbarPopup',
dataAttr: '[title]toolbar.sharedVideoMutedPopup',
dataAttrPosition: 'w'
}
]
},
'sip': {
id: 'toolbar_button_sip',
tooltipKey: 'toolbar.sip',
className: 'button icon-telephone',
hidden: true // will be displayed once
// the SIP calls functionality is detected
},
'dialpad': {
id: 'toolbar_button_dialpad',
tooltipKey: 'toolbar.dialpad',
className: 'button icon-dialpad',
//TODO: remove it after UI.updateDTMFSupport fix
hidden: true
}
};
function dialpadButtonClicked() {
//TODO show the dialpad box
}
function showSipNumberInput () {
let defaultNumber = config.defaultSipNumber
? config.defaultSipNumber
: '';
let titleKey = "dialog.sipMsg";
let msgString = (`
<input class="input-control"
name="sipNumber" type="text"
value="${defaultNumber}" autofocus>`);
APP.UI.messageHandler.openTwoButtonDialog({
titleKey,
msgString,
leftButtonKey: "dialog.Dial",
submitFunction: function (e, v, m, f) {
if (v && f.sipNumber) {
emitter.emit(UIEvents.SIP_DIAL, f.sipNumber);
}
},
focus: ':input:first'
});
}
/**
* Get place for toolbar button.
* Now it can be in main toolbar or in extended (left) toolbar
*
* @param btn {string}
* @returns {string}
*/
function getToolbarButtonPlace (btn) {
return interfaceConfig.MAIN_TOOLBAR_BUTTONS.includes(btn) ?
'main' :
'extended';
}
/**
* Event handler for side toolbar container toggled event.
*
* @param {string} containerId - ID of the container.
* @param {boolean} isVisible - Flag showing whether container
* is visible.
* @returns {void}
*/
function onSideToolbarContainerToggled(containerId, isVisible) {
Toolbar._handleSideToolbarContainerToggled(containerId, isVisible);
}
/**
* Event handler for local raise hand changed event.
*
* @param {boolean} isRaisedHand - Flag showing whether hand is raised.
* @returns {void}
*/
function onLocalRaiseHandChanged(isRaisedHand) {
Toolbar._setToggledState("toolbar_button_raisehand", isRaisedHand);
}
/**
* Event handler for full screen toggled event.
*
* @param {boolean} isFullScreen - Flag showing whether app in full
* screen mode.
* @returns {void}
*/
function onFullScreenToggled(isFullScreen) {
Toolbar._handleFullScreenToggled(isFullScreen);
}
Toolbar = {
init (eventEmitter) {
emitter = eventEmitter;
// The toolbar is enabled by default.
this.enabled = true;
this.toolbarSelector = $("#mainToolbarContainer");
this.extendedToolbarSelector = $("#extendedToolbar");
// Unregister listeners in case of reinitialization.
this.unregisterListeners();
// Initialise the toolbar buttons.
// The main toolbar will only take into account
// it's own configuration from interface_config.
this._initToolbarButtons();
this._setShortcutsAndTooltips();
this._setButtonHandlers();
this.registerListeners();
APP.UI.addListener(UIEvents.SHOW_CUSTOM_TOOLBAR_BUTTON_POPUP,
(popupID, show, timeout) => {
Toolbar._showCustomToolbarPopup(popupID, show, timeout);
});
if(!APP.tokenData.isGuest) {
$("#toolbar_button_profile").addClass("unclickable");
UIUtil.removeTooltip(
document.getElementById('toolbar_button_profile'));
}
},
/**
* Register listeners for UI events of toolbar component.
*
* @returns {void}
*/
registerListeners() {
APP.UI.addListener(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
onSideToolbarContainerToggled);
APP.UI.addListener(UIEvents.LOCAL_RAISE_HAND_CHANGED,
onLocalRaiseHandChanged);
APP.UI.addListener(UIEvents.FULLSCREEN_TOGGLED, onFullScreenToggled);
},
/**
* Unregisters handlers for UI events of Toolbar component.
*
* @returns {void}
*/
unregisterListeners() {
APP.UI.removeListener(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
onSideToolbarContainerToggled);
APP.UI.removeListener(UIEvents.LOCAL_RAISE_HAND_CHANGED,
onLocalRaiseHandChanged);
APP.UI.removeListener(UIEvents.FULLSCREEN_TOGGLED,
onFullScreenToggled);
},
/**
* Enables / disables the toolbar.
* @param {e} set to {true} to enable the toolbar or {false}
* to disable it
*/
enable (e) {
this.enabled = e;
if (!e && this.isVisible())
this.hide(false);
},
/**
* Indicates if the bottom toolbar is currently enabled.
* @return {this.enabled}
*/
isEnabled() {
return this.enabled;
},
showEtherpadButton () {
if (!$('#toolbar_button_etherpad').is(":visible")) {
$('#toolbar_button_etherpad').css({display: 'inline-block'});
}
},
// Shows or hides the 'shared video' button.
showSharedVideoButton () {
let id = 'toolbar_button_sharedvideo';
let shouldShow = UIUtil.isButtonEnabled('sharedvideo')
&& !config.disableThirdPartyRequests;
if (shouldShow) {
let el = document.getElementById(id);
UIUtil.setTooltip(el, 'toolbar.sharedvideo', 'right');
}
UIUtil.setVisible(id, shouldShow);
},
// checks whether desktop sharing is enabled and whether
// we have params to start automatically sharing
checkAutoEnableDesktopSharing () {
if (UIUtil.isButtonEnabled('desktop')
&& config.autoEnableDesktopSharing) {
emitter.emit(UIEvents.TOGGLE_SCREENSHARING);
}
},
// Shows or hides SIP calls button
showSipCallButton (show) {
let shouldShow = APP.conference.sipGatewayEnabled()
&& UIUtil.isButtonEnabled('sip') && show;
let id = 'toolbar_button_sip';
UIUtil.setVisible(id, shouldShow);
},
// Shows or hides the dialpad button
showDialPadButton (show) {
let shouldShow = UIUtil.isButtonEnabled('dialpad') && show;
let id = 'toolbar_button_dialpad';
UIUtil.setVisible(id, shouldShow);
},
/**
* Update the state of the button. The button has blue glow if desktop
* streaming is active.
*/
updateDesktopSharingButtonState () {
this._setToggledState( "toolbar_button_desktopsharing",
APP.conference.isSharingScreen);
},
/**
* Marks video icon as muted or not.
*
* @param {boolean} muted if icon should look like muted or not
*/
toggleVideoIcon (muted) {
$('#toolbar_button_camera').toggleClass("icon-camera-disabled", muted);
this._setToggledState("toolbar_button_camera", muted);
},
/**
* Enables / disables audio toolbar button.
*
* @param {boolean} enabled indicates if the button should be enabled
* or disabled
*/
setVideoIconEnabled (enabled) {
this._setMediaIconEnabled(
'#toolbar_button_camera',
enabled,
/* data-i18n attribute value */
`[content]toolbar.${enabled ? 'videomute' : 'cameraDisabled'}`,
/* shortcut attribute value */
'toggleVideoPopover');
enabled || this.toggleVideoIcon(!enabled);
},
/**
* Enables/disables the toolbar button associated with a specific media such
* as audio or video.
*
* @param {string} btn - The jQuery selector <tt>string</tt> which
* identifies the toolbar button to be enabled/disabled.
* @param {boolean} enabled - <tt>true</tt> to enable the specified
* <tt>btn</tt> or <tt>false</tt> to disable it.
* @param {string} dataI18n - The value to assign to the <tt>data-i18n</tt>
* attribute of the specified <tt>btn</tt>.
* @param {string} shortcut - The value, if any, to assign to the
* <tt>shortcut</tt> attribute of the specified <tt>btn</tt> if the toolbar
* button is enabled.
*/
_setMediaIconEnabled(btn, enabled, dataI18n, shortcut) {
const $btn = $(btn);
$btn
.prop('disabled', !enabled)
.attr('data-i18n', dataI18n)
.attr('shortcut', enabled && shortcut ? shortcut : '');
enabled
? $btn.removeAttr('disabled')
: $btn.attr('disabled', 'disabled');
APP.translation.translateElement($btn);
},
/**
* Marks audio icon as muted or not.
*
* @param {boolean} muted if icon should look like muted or not
*/
toggleAudioIcon(muted) {
$('#toolbar_button_mute')
.toggleClass("icon-microphone", !muted)
.toggleClass("icon-mic-disabled", muted);
this._setToggledState("toolbar_button_mute", muted);
},
/**
* Enables / disables audio toolbar button.
*
* @param {boolean} enabled indicates if the button should be enabled
* or disabled
*/
setAudioIconEnabled (enabled) {
this._setMediaIconEnabled(
'#toolbar_button_mute',
enabled,
/* data-i18n attribute value */
`[content]toolbar.${enabled ? 'mute' : 'micDisabled'}`,
/* shortcut attribute value */
'mutePopover');
enabled || this.toggleAudioIcon(!enabled);
},
/**
* Indicates if the toolbar is currently hovered.
* @return {boolean} true if the toolbar is currently hovered,
* false otherwise
*/
isHovered() {
var hovered = false;
this.toolbarSelector.find('*').each(function () {
let id = $(this).attr('id');
if ($(`#${id}:hover`).length > 0) {
hovered = true;
// break each
return false;
}
});
if (hovered)
return true;
if ($("#bottomToolbar:hover").length > 0
|| $("#extendedToolbar:hover").length > 0
|| SideContainerToggler.isHovered()) {
return true;
}
return false;
},
/**
* Returns true if this toolbar is currently visible, or false otherwise.
* @return <tt>true</tt> if currently visible, <tt>false</tt> - otherwise
*/
isVisible() {
return this.toolbarSelector.hasClass("fadeIn");
},
/**
* Hides the toolbar with animation or not depending on the animate
* parameter.
*/
hide() {
this.toolbarSelector
.removeClass("fadeIn")
.addClass("fadeOut");
let slideInAnimation = (SideContainerToggler.isVisible)
? "slideInExtX"
: "slideInX";
let slideOutAnimation = (SideContainerToggler.isVisible)
? "slideOutExtX"
: "slideOutX";
this.extendedToolbarSelector.toggleClass(slideInAnimation)
.toggleClass(slideOutAnimation);
},
/**
* Shows the toolbar with animation or not depending on the animate
* parameter.
*/
show() {
if (this.toolbarSelector.hasClass("fadeOut")) {
this.toolbarSelector.removeClass("fadeOut");
}
let slideInAnimation = (SideContainerToggler.isVisible)
? "slideInExtX"
: "slideInX";
let slideOutAnimation = (SideContainerToggler.isVisible)
? "slideOutExtX"
: "slideOutX";
if (this.extendedToolbarSelector.hasClass(slideOutAnimation)) {
this.extendedToolbarSelector.toggleClass(slideOutAnimation);
}
this.toolbarSelector.addClass("fadeIn");
this.extendedToolbarSelector.toggleClass(slideInAnimation);
},
registerClickListeners(listener) {
$('#mainToolbarContainer').click(listener);
$("#extendedToolbar").click(listener);
},
/**
* Handles the side toolbar toggle.
*
* @param {string} containerId the identifier of the container element
*/
_handleSideToolbarContainerToggled(containerId) {
Object.keys(defaultToolbarButtons).forEach(
id => {
if (!UIUtil.isButtonEnabled(id))
return;
var button = defaultToolbarButtons[id];
if (button.sideContainerId
&& button.sideContainerId === containerId) {
UIUtil.buttonClick(button.id, "selected");
return;
}
}
);
},
/**
* Handles full screen toggled.
*
* @param {boolean} isFullScreen indicates if we're currently in full
* screen mode
*/
_handleFullScreenToggled(isFullScreen) {
let element
= document.getElementById("toolbar_button_fullScreen");
element.className = isFullScreen
? element.className
.replace("icon-full-screen", "icon-exit-full-screen")
: element.className
.replace("icon-exit-full-screen", "icon-full-screen");
Toolbar._setToggledState("toolbar_button_fullScreen", isFullScreen);
},
/**
* Initialise toolbar buttons.
*/
_initToolbarButtons() {
interfaceConfig.TOOLBAR_BUTTONS.forEach((value, index) => {
let place = getToolbarButtonPlace(value);
if (value && value in defaultToolbarButtons) {
let button = defaultToolbarButtons[value];
this._addToolbarButton(
button,
place,
(interfaceConfig.MAIN_TOOLBAR_SPLITTER_INDEX !== undefined
&& index
=== interfaceConfig.MAIN_TOOLBAR_SPLITTER_INDEX));
}
});
},
/**
* Adds the given button to the main (top) or extended (left) toolbar.
*
* @param {Object} the button to add.
* @param {boolean} isFirst indicates if this is the first button in the
* toolbar
* @param {boolean} isLast indicates if this is the last button in the
* toolbar
* @param {boolean} isSplitter if this button is a splitter button for
* the dialog, which means that a special splitter style will be applied
*/
_addToolbarButton(button, place, isSplitter) {
const places = {
main: 'mainToolbar',
extended: 'extendedToolbarButtons'
};
let id = places[place];
let buttonElement = document.createElement("a");
if (button.className) {
buttonElement.className = button.className;
}
if (isSplitter) {
let splitter = document.createElement('span');
splitter.className = 'toolbar__splitter';
document.getElementById(id).appendChild(splitter);
}
buttonElement.id = button.id;
if (button.html)
buttonElement.innerHTML = button.html;
//TODO: remove it after UI.updateDTMFSupport fix
if (button.hidden)
buttonElement.style.display = 'none';
if (button.shortcutAttr)
buttonElement.setAttribute("shortcut", button.shortcutAttr);
if (button.content)
buttonElement.setAttribute("content", button.content);
if (button.i18n)
buttonElement.setAttribute("data-i18n", button.i18n);
buttonElement.setAttribute("data-container", "body");
buttonElement.setAttribute("data-placement", "bottom");
this._addPopups(buttonElement, button.popups);
document.getElementById(id)
.appendChild(buttonElement);
},
_addPopups(buttonElement, popups = []) {
popups.forEach((popup) => {
const popupElement = document.createElement('div');
popupElement.id = popup.id;
popupElement.className = popup.className;
popupElement.setAttribute('data-i18n', popup.dataAttr);
let gravity = 'n';
if (popup.dataAttrPosition)
gravity = popup.dataAttrPosition;
// use custom attribute to save gravity option
// we use 'data-tooltip' in UIUtil to activate all tooltips
// but we want these to be manually triggered
popupElement.setAttribute('tooltip-gravity', gravity);
APP.translation.translateElement($(popupElement));
buttonElement.appendChild(popupElement);
});
},
/**
* Show custom popup/tooltip for a specified button.
* @param popupSelectorID the selector id of the popup to show
* @param show true or false/show or hide the popup
* @param timeout the time to show the popup
*/
_showCustomToolbarPopup(popupSelectorID, show, timeout) {
const gravity = $(popupSelectorID).attr('tooltip-gravity');
AJS.$(popupSelectorID)
.tooltip({
trigger: 'manual',
html: true,
gravity: gravity,
title: 'title'});
if (show) {
AJS.$(popupSelectorID).tooltip('show');
setTimeout(function () {
// hide the tooltip
AJS.$(popupSelectorID).tooltip('hide');
}, timeout);
} else {
AJS.$(popupSelectorID).tooltip('hide');
}
},
/**
* Sets the toggled state of the given element depending on the isToggled
* parameter.
*
* @param elementId the element identifier
* @param isToggled indicates if the element should be toggled or untoggled
*/
_setToggledState(elementId, isToggled) {
$("#" + elementId).toggleClass("toggled", isToggled);
},
/**
* Sets Shortcuts and Tooltips for all toolbar buttons
*
* @private
*/
_setShortcutsAndTooltips() {
Object.keys(defaultToolbarButtons).forEach(
id => {
if (UIUtil.isButtonEnabled(id)) {
let button = defaultToolbarButtons[id];
let buttonElement = document.getElementById(button.id);
if (!buttonElement) return false;
let tooltipPosition
= (interfaceConfig.MAIN_TOOLBAR_BUTTONS
.indexOf(id) > -1)
? "bottom" : "right";
UIUtil.setTooltip( buttonElement,
button.tooltipKey,
tooltipPosition);
if (button.shortcut)
APP.keyboardshortcut.registerShortcut(
button.shortcut,
button.shortcutAttr,
button.shortcutFunc,
button.shortcutDescription
);
}
}
);
},
/**
* Sets Handlers for all toolbar buttons
*
* @private
*/
_setButtonHandlers() {
Object.keys(buttonHandlers).forEach(
buttonId => $(`#${buttonId}`).click(function(event) {
!$(this).prop('disabled') && buttonHandlers[buttonId](event);
})
);
}
};
export default Toolbar;

View File

@ -1,149 +0,0 @@
/* global APP, config, $, interfaceConfig */
import UIUtil from '../util/UIUtil';
import Toolbar from './Toolbar';
import SideContainerToggler from "../side_pannels/SideContainerToggler";
let toolbarTimeoutObject;
let toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
/**
* If true the toolbar will be always displayed
*/
let alwaysVisibleToolbar = false;
function showDesktopSharingButton() {
if (APP.conference.isDesktopSharingEnabled &&
UIUtil.isButtonEnabled('desktop')) {
$('#toolbar_button_desktopsharing').css({display: "inline-block"});
} else {
$('#toolbar_button_desktopsharing').css({display: "none"});
}
}
/**
* Hides the toolbar.
*
* @param force {true} to force the hiding of the toolbar without caring about
* the extended toolbar side panels.
*/
function hideToolbar(force) { // eslint-disable-line no-unused-vars
if (alwaysVisibleToolbar) {
return;
}
clearTimeout(toolbarTimeoutObject);
toolbarTimeoutObject = null;
if (force !== true &&
(Toolbar.isHovered()
|| APP.UI.isRingOverlayVisible()
|| SideContainerToggler.isVisible())) {
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
} else {
Toolbar.hide();
$('#subject').animate({top: "-=40"}, 300);
}
}
const ToolbarToggler = {
/**
* Initializes the ToolbarToggler
*/
init() {
alwaysVisibleToolbar = (config.alwaysVisibleToolbar === true);
// disabled
//this._registerWindowClickListeners();
},
/**
* Registers click listeners handling the show and hode of toolbars when
* user clicks outside of toolbar area.
*/
_registerWindowClickListeners() {
$(window).click(function() {
(Toolbar.isEnabled() && Toolbar.isVisible())
? hideToolbar(true)
: this.showToolbar();
}.bind(this));
Toolbar.registerClickListeners(function(event){
event.stopPropagation();
});
},
/**
* Sets the value of alwaysVisibleToolbar variable.
* @param value {boolean} the new value of alwaysVisibleToolbar variable
*/
setAlwaysVisibleToolbar(value) {
alwaysVisibleToolbar = value;
},
/**
* Resets the value of alwaysVisibleToolbar variable to the default one.
*/
resetAlwaysVisibleToolbar() {
alwaysVisibleToolbar = (config.alwaysVisibleToolbar === true);
},
/**
* Shows the main toolbar.
* @param timeout (optional) to specify custom timeout value
*/
showToolbar (timeout) {
if (interfaceConfig.filmStripOnly) {
return;
}
var updateTimeout = false;
if (Toolbar.isEnabled() && !Toolbar.isVisible()) {
Toolbar.show();
$('#subject').animate({top: "+=40"}, 300);
updateTimeout = true;
}
if (updateTimeout) {
if (toolbarTimeoutObject) {
clearTimeout(toolbarTimeoutObject);
toolbarTimeoutObject = null;
}
toolbarTimeoutObject
= setTimeout(hideToolbar, timeout || toolbarTimeout);
toolbarTimeout = interfaceConfig.TOOLBAR_TIMEOUT;
}
// Show/hide desktop sharing button
showDesktopSharingButton();
},
/**
* Docks/undocks the toolbar.
*
* @param isDock indicates what operation to perform
*/
dockToolbar (isDock) {
if (interfaceConfig.filmStripOnly || !Toolbar.isEnabled()) {
return;
}
if (isDock) {
// First make sure the toolbar is shown.
if (!Toolbar.isVisible()) {
this.showToolbar();
}
// Then clear the time out, to dock the toolbar.
clearTimeout(toolbarTimeoutObject);
toolbarTimeoutObject = null;
} else {
if (Toolbar.isVisible()) {
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
} else {
this.showToolbar();
}
}
}
};
module.exports = ToolbarToggler;

View File

@ -6,7 +6,7 @@ import { DialogContainer } from '../../base/dialog';
import { Container } from '../../base/react'; import { Container } from '../../base/react';
import { FilmStrip } from '../../film-strip'; import { FilmStrip } from '../../film-strip';
import { LargeVideo } from '../../large-video'; import { LargeVideo } from '../../large-video';
import { Toolbar } from '../../toolbar'; import { setToolbarVisible, Toolbar } from '../../toolbar';
import { styles } from './styles'; import { styles } from './styles';
@ -28,8 +28,39 @@ class Conference extends Component {
* @static * @static
*/ */
static propTypes = { static propTypes = {
dispatch: React.PropTypes.func /**
} * The handler which dispatches the (redux) action connect.
*
* @private
* @type {Function}
*/
_onConnect: React.PropTypes.func,
/**
* The handler which dispatches the (redux) action disconnect.
*
* @private
* @type {Function}
*/
_onDisconnect: React.PropTypes.func,
/**
* The handler which dispatches the (redux) action setTooblarVisible to
* show/hide the toolbar.
*
* @private
* @type {boolean}
*/
_setToolbarVisible: React.PropTypes.func,
/**
* The indicator which determines whether toolbar is visible.
*
* @private
* @type {boolean}
*/
_toolbarVisible: React.PropTypes.bool
};
/** /**
* Initializes a new Conference instance. * Initializes a new Conference instance.
@ -40,8 +71,6 @@ class Conference extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { toolbarVisible: true };
/** /**
* The numerical ID of the timeout in milliseconds after which the * The numerical ID of the timeout in milliseconds after which the
* toolbar will be hidden. To be used with * toolbar will be hidden. To be used with
@ -62,7 +91,7 @@ class Conference extends Component {
* returns {void} * returns {void}
*/ */
componentDidMount() { componentDidMount() {
this._setToolbarTimeout(this.state.toolbarVisible); this._setToolbarTimeout(this.props._toolbarVisible);
} }
/** /**
@ -72,7 +101,7 @@ class Conference extends Component {
* @returns {void} * @returns {void}
*/ */
componentWillMount() { componentWillMount() {
this.props.dispatch(connect()); this.props._onConnect();
} }
/** /**
@ -85,7 +114,7 @@ class Conference extends Component {
componentWillUnmount() { componentWillUnmount() {
this._clearToolbarTimeout(); this._clearToolbarTimeout();
this.props.dispatch(disconnect()); this.props._onDisconnect();
} }
/** /**
@ -95,8 +124,6 @@ class Conference extends Component {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const toolbarVisible = this.state.toolbarVisible;
return ( return (
<Container <Container
onClick = { this._onClick } onClick = { this._onClick }
@ -104,11 +131,11 @@ class Conference extends Component {
touchFeedback = { false }> touchFeedback = { false }>
<LargeVideo /> <LargeVideo />
<Toolbar visible = { toolbarVisible } />
<FilmStrip visible = { !toolbarVisible } /> <Toolbar />
<FilmStrip />
<DialogContainer /> <DialogContainer />
</Container> </Container>
); );
} }
@ -135,10 +162,9 @@ class Conference extends Component {
* @returns {void} * @returns {void}
*/ */
_onClick() { _onClick() {
const toolbarVisible = !this.state.toolbarVisible; const toolbarVisible = !this.props._toolbarVisible;
this.setState({ toolbarVisible });
this.props._setToolbarVisible(toolbarVisible);
this._setToolbarTimeout(toolbarVisible); this._setToolbarTimeout(toolbarVisible);
} }
@ -159,4 +185,73 @@ class Conference extends Component {
} }
} }
export default reactReduxConnect()(Conference); /**
* Maps dispatching of some action to React component props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @private
* @returns {{
* _onConnect: Function,
* _onDisconnect: Function,
* _setToolbarVisible: Function
* }}
*/
function _mapDispatchToProps(dispatch) {
return {
/**
* Dispatched an action connecting to the conference.
*
* @returns {Object} Dispatched action.
* @private
*/
_onConnect() {
return dispatch(connect());
},
/**
* Dispatches an action disconnecting from the conference.
*
* @returns {Object} Dispatched action.
* @private
*/
_onDisconnect() {
return dispatch(disconnect());
},
/**
* Dispatched an action changing visiblity of the toolbar.
*
* @param {boolean} isVisible - Flag showing whether toolbar is
* visible.
* @returns {Object} Dispatched action.
* @private
*/
_setToolbarVisible(isVisible: boolean) {
return dispatch(setToolbarVisible(isVisible));
}
};
}
/**
* Maps (parts of) the Redux state to the associated Conference's props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _toolbarVisible: boolean
* }}
*/
function _mapStateToProps(state) {
return {
/**
* The indicator which determines whether toolbar is visible.
*
* @private
* @type {boolean}
*/
_toolbarVisible: state['features/toolbar'].visible
};
}
export default reactReduxConnect(_mapStateToProps, _mapDispatchToProps)(
Conference);

View File

@ -6,9 +6,8 @@ import { connect as reactReduxConnect } from 'react-redux';
import { connect, disconnect } from '../../base/connection'; import { connect, disconnect } from '../../base/connection';
import { DialogContainer } from '../../base/dialog'; import { DialogContainer } from '../../base/dialog';
import { Watermarks } from '../../base/react'; import { Watermarks } from '../../base/react';
import { FeedbackButton } from '../../feedback';
import { OverlayContainer } from '../../overlay'; import { OverlayContainer } from '../../overlay';
import { Notice } from '../../toolbar'; import { Toolbar } from '../../toolbar';
import { HideNotificationBarStyle } from '../../unsupported-browser'; import { HideNotificationBarStyle } from '../../unsupported-browser';
declare var $: Function; declare var $: Function;
@ -66,25 +65,8 @@ class Conference extends Component {
render() { render() {
return ( return (
<div id = 'videoconference_page'> <div id = 'videoconference_page'>
<div id = 'mainToolbarContainer'> <Toolbar />
<Notice />
<div
className = 'toolbar'
id = 'mainToolbar' />
</div>
<div
className = 'hide'
id = 'subject' />
<div
className = 'toolbar'
id = 'extendedToolbar'>
<div id = 'extendedToolbarButtons' />
<FeedbackButton />
<div id = 'sideToolbarContainer' />
</div>
<div id = 'videospace'> <div id = 'videospace'>
<div <div
className = 'videocontainer' className = 'videocontainer'
@ -154,6 +136,7 @@ class Conference extends Component {
</div> </div>
</div> </div>
</div> </div>
<DialogContainer /> <DialogContainer />
<OverlayContainer /> <OverlayContainer />
<HideNotificationBarStyle /> <HideNotificationBarStyle />

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { ScrollView } from 'react-native'; import { ScrollView } from 'react-native';
import { connect } from 'react-redux';
import { Container } from '../../base/react'; import { Container } from '../../base/react';
@ -33,7 +33,7 @@ class FilmStrip extends Component {
* @private * @private
* @type {boolean} * @type {boolean}
*/ */
visible: React.PropTypes.bool.isRequired _visible: React.PropTypes.bool.isRequired
} }
/** /**
@ -45,7 +45,7 @@ class FilmStrip extends Component {
return ( return (
<Container <Container
style = { styles.filmStrip } style = { styles.filmStrip }
visible = { this.props.visible }> visible = { this.props._visible }>
<ScrollView <ScrollView
// eslint-disable-next-line react/jsx-curly-spacing // eslint-disable-next-line react/jsx-curly-spacing
@ -109,6 +109,7 @@ class FilmStrip extends Component {
* @private * @private
* @returns {{ * @returns {{
* _participants: Participant[], * _participants: Participant[],
* _visible: boolean
* }} * }}
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state) {
@ -119,7 +120,19 @@ function _mapStateToProps(state) {
* @private * @private
* @type {Participant[]} * @type {Participant[]}
*/ */
_participants: state['features/base/participants'] _participants: state['features/base/participants'],
/**
* The indicator which determines whether the film strip is visible.
*
* XXX The React Component FilmStrip is used on mobile only at the time
* of this writing and on mobile the film strip is visible when the
* toolbar is not.
*
* @private
* @type {boolean}
*/
_visible: !state['features/toolbar'].visible
}; };
} }

View File

@ -0,0 +1,98 @@
import { Symbol } from '../base/react';
/**
* The type of the action which signals that toolbar timeout should be changed.
*
* {
* type: CLEAR_TOOLBAR_TIMEOUT
* }
*/
export const CLEAR_TOOLBAR_TIMEOUT = Symbol('CLEAR_TOOLBAR_TIMEOUT');
/**
* The type of the action which signals that a value for always visible toolbar
* should be changed.
*
* {
* type: SET_ALWAYS_VISIBLE_TOOLBAR,
* alwaysVisible: boolean
* }
*/
export const SET_ALWAYS_VISIBLE_TOOLBAR = Symbol('SET_ALWAYS_VISIBLE_TOOLBAR');
/**
* The type of the action which signals that a value for conference subject
* should be changed.
*
* {
* type: SET_SUBJECT,
* subject: string
* }
*/
export const SET_SUBJECT = Symbol('SET_SUBJECT');
/**
* The type of the action which signals that a value of subject slide in should
* be changed.
*
* {
* type: SET_SUBJECT_SLIDE_IN,
* subjectSlideIn: boolean
* }
*/
export const SET_SUBJECT_SLIDE_IN = Symbol('SET_SUBJECT_SLIDE_IN');
/**
* The type of the action which signals that a value for toolbar button should
* be changed.
*
* {
* type: SET_TOOLBAR_BUTTON,
* button: Object,
* key: string
* }
*/
export const SET_TOOLBAR_BUTTON = Symbol('SET_TOOLBAR_BUTTON');
/**
* The type of the action which signals that toolbar is/isn't being hovered.
*
* {
* type: SET_TOOLBAR_HOVERED,
* hovered: boolean
* }
*/
export const SET_TOOLBAR_HOVERED = Symbol('SET_TOOLBAR_HOVERED');
/**
* The type of the action which signals that new toolbar timeout should be set
* and the value of toolbar timeout should be changed.
*
* {
* type: SET_TOOLBAR_TIMEOUT,
* handler: Function,
toolbarTimeout: number
* }
*/
export const SET_TOOLBAR_TIMEOUT = Symbol('SET_TOOLBAR_TIMEOUT');
/**
* The type of the action which signals that value of toolbar timeout should
* be changed.
*
* {
* type: SET_TOOLBAR_TIMEOUT_NUMBER,
* toolbarTimeout: number
* }
*/
export const SET_TOOLBAR_TIMEOUT_NUMBER = Symbol('SET_TOOLBAR_TIMEOUT');
/**
* The type of the (redux) action which shows/hides the toolbar.
*
* {
* type: SET_TOOLBAR_VISIBLE,
* visible: boolean
* }
*/
export const SET_TOOLBAR_VISIBLE = Symbol('SET_TOOLBAR_VISIBLE');

View File

@ -0,0 +1,277 @@
/* @flow */
import type { Dispatch } from 'redux-thunk';
import {
CLEAR_TOOLBAR_TIMEOUT,
SET_ALWAYS_VISIBLE_TOOLBAR,
SET_SUBJECT,
SET_SUBJECT_SLIDE_IN,
SET_TOOLBAR_BUTTON,
SET_TOOLBAR_HOVERED,
SET_TOOLBAR_TIMEOUT,
SET_TOOLBAR_TIMEOUT_NUMBER,
SET_TOOLBAR_VISIBLE
} from './actionTypes';
/**
* Event handler for local raise hand changed event.
*
* @param {boolean} handRaised - Flag showing whether hand is raised.
* @returns {Function}
*/
export function changeLocalRaiseHand(handRaised: boolean): Function {
return (dispatch: Dispatch<*>, getState: Function) => {
const state = getState();
const { secondaryToolbarButtons } = state['features/toolbar'];
const buttonName = 'raisehand';
const button = secondaryToolbarButtons.get(buttonName);
button.toggled = handRaised;
dispatch(setToolbarButton(buttonName, button));
};
}
/**
* Signals that toolbar timeout should be cleared.
*
* @returns {{
* type: CLEAR_TOOLBAR_TIMEOUT
* }}
*/
export function clearToolbarTimeout(): Object {
return {
type: CLEAR_TOOLBAR_TIMEOUT
};
}
/**
* Signals that always visible toolbars value should be changed.
*
* @param {boolean} alwaysVisible - Value to be set in redux store.
* @returns {{
* type: SET_ALWAYS_VISIBLE_TOOLBAR,
* alwaysVisible: bool
* }}
*/
export function setAlwaysVisibleToolbar(alwaysVisible: boolean): Object {
return {
type: SET_ALWAYS_VISIBLE_TOOLBAR,
alwaysVisible
};
}
/**
* Enables / disables audio toolbar button.
*
* @param {boolean} enabled - Indicates if the button should be enabled
* or disabled.
* @returns {Function}
*/
export function setAudioIconEnabled(enabled: boolean = false): Function {
return (dispatch: Dispatch<*>) => {
const i18nKey = enabled ? 'mute' : 'micDisabled';
const i18n = `[content]toolbar.${i18nKey}`;
const button = {
enabled,
i18n,
toggled: !enabled
};
dispatch(setToolbarButton('microphone', button));
};
}
/**
* Signals that value of conference subject should be changed.
*
* @param {string} subject - Conference subject string.
* @returns {Object}
*/
export function setSubject(subject: string) {
return {
type: SET_SUBJECT,
subject
};
}
/**
* Signals that toolbar subject slide in value should be changed.
*
* @param {boolean} subjectSlideIn - Flag showing whether subject is shown.
* @returns {{
* type: SET_SUBJECT_SLIDE_IN,
* subjectSlideIn: boolean
* }}
*/
export function setSubjectSlideIn(subjectSlideIn: boolean): Object {
return {
type: SET_SUBJECT_SLIDE_IN,
subjectSlideIn
};
}
/**
* Signals that value of the button specified by key should be changed.
*
* @param {string} buttonName - Button key.
* @param {Object} button - Button object.
* @returns {{
* type: SET_TOOLBAR_BUTTON,
* buttonName: string,
* button: Object
* }}
*/
export function setToolbarButton(buttonName: string, button: Object): Object {
return {
type: SET_TOOLBAR_BUTTON,
buttonName,
button
};
}
/**
* Signals that toolbar is hovered value should be changed.
*
* @param {boolean} hovered - Flag showing whether toolbar is hovered.
* @returns {{
* type: SET_TOOLBAR_HOVERED,
* hovered: boolean
* }}
*/
export function setToolbarHovered(hovered: boolean): Object {
return {
type: SET_TOOLBAR_HOVERED,
hovered
};
}
/**
* Dispatches an action which sets new timeout and clears the previous one.
*
* @param {Function} handler - Function to be invoked after the timeout.
* @param {number} toolbarTimeout - Delay.
* @returns {{
* type: SET_TOOLBAR_TIMEOUT,
* handler: Function,
* toolbarTimeout: number
* }}
*/
export function setToolbarTimeout(handler: Function,
toolbarTimeout: number): Object {
return {
type: SET_TOOLBAR_TIMEOUT,
handler,
toolbarTimeout
};
}
/**
* Dispatches an action which sets new toolbar timeout value.
*
* @param {number} toolbarTimeout - Delay.
* @returns {{
* type: SET_TOOLBAR_TIMEOUT_NUMBER,
* toolbarTimeout: number
* }}
*/
export function setToolbarTimeoutNumber(toolbarTimeout: number): Object {
return {
type: SET_TOOLBAR_TIMEOUT_NUMBER,
toolbarTimeout
};
}
/**
* Shows/hides the toolbar.
*
* @param {boolean} visible - True to show the toolbar or false to hide it.
* @returns {{
* type: SET_TOOLBAR_VISIBLE,
* visible: boolean
* }}
*/
export function setToolbarVisible(visible: boolean): Object {
return {
type: SET_TOOLBAR_VISIBLE,
visible
};
}
/**
* Enables / disables audio toolbar button.
*
* @param {boolean} enabled - Indicates if the button should be enabled
* or disabled.
* @returns {Function}
*/
export function setVideoIconEnabled(enabled: boolean = false): Function {
return (dispatch: Dispatch<*>) => {
const i18nKey = enabled ? 'videomute' : 'cameraDisabled';
const i18n = `[content]toolbar.${i18nKey}`;
const button = {
enabled,
i18n,
toggled: !enabled
};
dispatch(setToolbarButton('camera', button));
};
}
/**
* Shows etherpad button if it's not shown.
*
* @returns {Function}
*/
export function showEtherpadButton(): Function {
return (dispatch: Dispatch<*>) => {
dispatch(setToolbarButton('etherpad', {
hidden: false
}));
};
}
/**
* Event handler for full screen toggled event.
*
* @param {boolean} isFullScreen - Flag showing whether app in full
* screen mode.
* @returns {Function}
*/
export function toggleFullScreen(isFullScreen: boolean): Function {
return (dispatch: Dispatch<*>, getState: Function) => {
const state = getState();
const { primaryToolbarButtons } = state['features/toolbar'];
const buttonName = 'fullscreen';
const button = primaryToolbarButtons.get(buttonName);
button.toggled = isFullScreen;
dispatch(setToolbarButton(buttonName, button));
};
}
/**
* Sets negation of button's toggle property.
*
* @param {string} buttonName - Button key.
* @returns {Function}
*/
export function toggleToolbarButton(buttonName: string): Function {
return (dispatch: Dispatch, getState: Function) => {
const state = getState();
const {
primaryToolbarButtons,
secondaryToolbarButtons
} = state['features/toolbar'];
const button
= primaryToolbarButtons.get(buttonName)
|| secondaryToolbarButtons.get(buttonName);
dispatch(setToolbarButton(buttonName, {
toggled: !button.toggled
}));
};
}

View File

@ -0,0 +1,291 @@
/* @flow */
import Recording from '../../../modules/UI/recording/Recording';
import SideContainerToggler
from '../../../modules/UI/side_pannels/SideContainerToggler';
import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../../../modules/UI/util/UIUtil';
import {
clearToolbarTimeout,
setAlwaysVisibleToolbar,
setSubjectSlideIn,
setToolbarButton,
setToolbarTimeout,
setToolbarTimeoutNumber,
setToolbarVisible,
toggleToolbarButton
} from './actions.native';
export * from './actions.native';
declare var $: Function;
declare var APP: Object;
declare var config: Object;
declare var interfaceConfig: Object;
/**
* Checks whether desktop sharing is enabled and whether
* we have params to start automatically sharing.
*
* @returns {Function}
*/
export function checkAutoEnableDesktopSharing(): Function {
return () => {
// XXX Should use dispatcher to toggle screensharing but screensharing
// hasn't been React-ified yet.
if (UIUtil.isButtonEnabled('desktop')
&& config.autoEnableDesktopSharing) {
APP.UI.eventEmitter.emit(UIEvents.TOGGLE_SCREENSHARING);
}
};
}
/**
* Docks/undocks toolbar based on its parameter.
*
* @param {boolean} dock - True if dock, false otherwise.
* @returns {Function}
*/
export function dockToolbar(dock: boolean): Function {
return (dispatch: Dispatch<*>, getState: Function) => {
if (interfaceConfig.filmStripOnly) {
return;
}
const state = getState();
const { toolbarTimeout, visible } = state['features/toolbar'];
if (dock) {
// First make sure the toolbar is shown.
visible || dispatch(showToolbar());
dispatch(clearToolbarTimeout());
} else if (visible) {
dispatch(
setToolbarTimeout(
() => dispatch(hideToolbar()),
toolbarTimeout));
} else {
dispatch(showToolbar());
}
};
}
/**
* Hides the toolbar.
*
* @param {boolean} force - True to force the hiding of the toolbar without
* caring about the extended toolbar side panels.
* @returns {Function}
*/
export function hideToolbar(force: boolean = false): Function {
return (dispatch: Dispatch<*>, getState: Function) => {
const state = getState();
const {
alwaysVisible,
hovered,
toolbarTimeout
} = state['features/toolbar'];
if (alwaysVisible) {
return;
}
dispatch(clearToolbarTimeout());
if (!force
&& (hovered
|| APP.UI.isRingOverlayVisible()
|| SideContainerToggler.isVisible())) {
dispatch(
setToolbarTimeout(
() => dispatch(hideToolbar()),
toolbarTimeout));
} else {
dispatch(setToolbarVisible(false));
dispatch(setSubjectSlideIn(false));
}
};
}
/**
* Action that reset always visible toolbar to default state.
*
* @returns {Function}
*/
export function resetAlwaysVisibleToolbar(): Function {
return (dispatch: Dispatch<*>) => {
const alwaysVisible = config.alwaysVisibleToolbar === true;
dispatch(setAlwaysVisibleToolbar(alwaysVisible));
};
}
/**
* Signals that unclickable property of profile button should change its value.
*
* @param {boolean} unclickable - Shows whether button is unclickable.
* @returns {Function}
*/
export function setProfileButtonUnclickable(unclickable: boolean): Function {
return (dispatch: Dispatch<*>) => {
const buttonName = 'profile';
dispatch(setToolbarButton(buttonName, {
unclickable
}));
UIUtil.removeTooltip(document.getElementById('toolbar_button_profile'));
};
}
/**
* Shows desktop sharing button.
*
* @returns {Function}
*/
export function showDesktopSharingButton(): Function {
return (dispatch: Dispatch<*>) => {
const buttonName = 'desktop';
const visible
= APP.conference.isDesktopSharingEnabled
&& UIUtil.isButtonEnabled(buttonName);
dispatch(setToolbarButton(buttonName, {
hidden: !visible
}));
};
}
/**
* Shows or hides the dialpad button.
*
* @param {boolean} show - Flag showing whether to show button or not.
* @returns {Function}
*/
export function showDialPadButton(show: boolean): Function {
return (dispatch: Dispatch<*>) => {
const buttonName = 'dialpad';
const shouldShow = UIUtil.isButtonEnabled(buttonName) && show;
if (shouldShow) {
dispatch(setToolbarButton(buttonName, {
hidden: false
}));
}
};
}
/**
* Shows recording button.
*
* @returns {Function}
*/
export function showRecordingButton(): Function {
return (dispatch: Dispatch<*>) => {
const eventEmitter = APP.UI.eventEmitter;
const buttonName = 'recording';
dispatch(setToolbarButton(buttonName, {
hidden: false
}));
Recording.init(eventEmitter, config.recordingType);
};
}
/**
* Shows or hides the 'shared video' button.
*
* @returns {Function}
*/
export function showSharedVideoButton(): Function {
return (dispatch: Dispatch<*>) => {
const buttonName = 'sharedvideo';
const shouldShow
= UIUtil.isButtonEnabled(buttonName)
&& !config.disableThirdPartyRequests;
if (shouldShow) {
dispatch(setToolbarButton(buttonName, {
hidden: false
}));
}
};
}
/**
* Shows SIP call button if it's required and appropriate
* flag is passed.
*
* @param {boolean} show - Flag showing whether to show button or not.
* @returns {Function}
*/
export function showSIPCallButton(show: boolean): Function {
return (dispatch: Dispatch<*>) => {
const buttonName = 'sip';
const shouldShow
= APP.conference.sipGatewayEnabled()
&& UIUtil.isButtonEnabled(buttonName)
&& show;
if (shouldShow) {
dispatch(setToolbarButton(buttonName, {
hidden: !shouldShow
}));
}
};
}
/**
* Shows the toolbar for specified timeout.
*
* @param {number} timeout - Timeout for showing the toolbar.
* @returns {Function}
*/
export function showToolbar(timeout: number = 0): Object {
return (dispatch: Dispatch<*>, getState: Function) => {
if (interfaceConfig.filmStripOnly) {
return;
}
const state = getState();
const { toolbarTimeout, visible } = state['features/toolbar'];
const finalTimeout = timeout || toolbarTimeout;
if (!visible) {
dispatch(setToolbarVisible(true));
dispatch(setSubjectSlideIn(true));
dispatch(
setToolbarTimeout(() => dispatch(hideToolbar()), finalTimeout));
dispatch(setToolbarTimeoutNumber(interfaceConfig.TOOLBAR_TIMEOUT));
}
};
}
/**
* Event handler for side toolbar container toggled event.
*
* @param {string} containerId - ID of the container.
* @returns {void}
*/
export function toggleSideToolbarContainer(containerId: string): Function {
return (dispatch: Dispatch, getState: Function) => {
const state = getState();
const { secondaryToolbarButtons } = state['features/toolbar'];
for (const key of secondaryToolbarButtons.keys()) {
const isButtonEnabled = UIUtil.isButtonEnabled(key);
const button = secondaryToolbarButtons.get(key);
if (isButtonEnabled
&& button.sideContainerId
&& button.sideContainerId === containerId) {
dispatch(toggleToolbarButton(key));
break;
}
}
};
}

View File

@ -1,165 +0,0 @@
import React, { Component } from 'react';
import { appNavigate } from '../../app';
import { toggleAudioMuted, toggleVideoMuted } from '../../base/media';
import { ColorPalette } from '../../base/styles';
import { beginRoomLockRequest } from '../../room-lock';
import { styles } from './styles';
/**
* Abstract (base) class for the conference toolbar.
*
* @abstract
*/
export class AbstractToolbar extends Component {
/**
* AbstractToolbar component's property types.
*
* @static
*/
static propTypes = {
_audioMuted: React.PropTypes.bool,
/**
* The indicator which determines whether the conference is
* locked/password-protected.
*
* @protected
* @type {boolean}
*/
_locked: React.PropTypes.bool,
_videoMuted: React.PropTypes.bool,
dispatch: React.PropTypes.func,
visible: React.PropTypes.bool.isRequired
}
/**
* Initializes a new AbstractToolbar instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onHangup = this._onHangup.bind(this);
this._onRoomLock = this._onRoomLock.bind(this);
this._toggleAudio = this._toggleAudio.bind(this);
this._toggleVideo = this._toggleVideo.bind(this);
}
/**
* Gets the styles for a button that toggles the mute state of a specific
* media type.
*
* @param {string} mediaType - The {@link MEDIA_TYPE} associated with the
* button to get styles for.
* @protected
* @returns {{
* iconName: string,
* iconStyle: Object,
* style: Object
* }}
*/
_getMuteButtonStyles(mediaType) {
let iconName;
let iconStyle;
let style = styles.primaryToolbarButton;
if (this.props[`_${mediaType}Muted`]) {
iconName = this[`${mediaType}MutedIcon`];
iconStyle = styles.whiteIcon;
style = {
...style,
backgroundColor: ColorPalette.buttonUnderlay
};
} else {
iconName = this[`${mediaType}Icon`];
iconStyle = styles.icon;
}
return {
iconName,
iconStyle,
style
};
}
/**
* Dispatches action to leave the current conference.
*
* @protected
* @returns {void}
*/
_onHangup() {
// XXX We don't know here which value is effectively/internally used
// when there's no valid room name to join. It isn't our business to
// know that anyway. The undefined value is our expression of (1) the
// lack of knowledge & (2) the desire to no longer have a valid room
// name to join.
this.props.dispatch(appNavigate(undefined));
}
/**
* Dispatches an action to set the lock i.e. password protection of the
* conference/room.
*
* @protected
* @returns {void}
*/
_onRoomLock() {
this.props.dispatch(beginRoomLockRequest());
}
/**
* Dispatches an action to toggle the mute state of the audio/microphone.
*
* @protected
* @returns {void}
*/
_toggleAudio() {
this.props.dispatch(toggleAudioMuted());
}
/**
* Dispatches an action to toggle the mute state of the video/camera.
*
* @protected
* @returns {void}
*/
_toggleVideo() {
this.props.dispatch(toggleVideoMuted());
}
}
/**
* Maps parts of media state to component props.
*
* @param {Object} state - Redux state.
* @protected
* @returns {{
* _audioMuted: boolean,
* _locked: boolean,
* _videoMuted: boolean
* }}
*/
export function _mapStateToProps(state) {
const conference = state['features/base/conference'];
const media = state['features/base/media'];
return {
_audioMuted: media.audio.muted,
/**
* The indicator which determines whether the conference is
* locked/password-protected.
*
* @protected
* @type {boolean}
*/
_locked: conference.locked,
_videoMuted: media.video.muted
};
}

View File

@ -0,0 +1,200 @@
/* @flow */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
setToolbarHovered
} from '../actions';
import ToolbarButton from './ToolbarButton';
declare var APP: Object;
declare var config: Object;
declare var interfaceConfig: Object;
/**
* Class implementing Primary Toolbar React component.
*
* @class PrimaryToolbar
* @extends Component
*/
class BaseToolbar extends Component {
_renderToolbarButton: Function;
/**
* Base toolbar component's property types.
*
* @static
*/
static propTypes = {
/**
* Handler for mouse out event.
*/
_onMouseOut: React.PropTypes.func,
/**
* Handler for mouse over event.
*/
_onMouseOver: React.PropTypes.func,
/**
* Contains button handlers.
*/
buttonHandlers: React.PropTypes.object,
/**
* Children of current React component.
*/
children: React.PropTypes.element,
/**
* Toolbar's class name.
*/
className: React.PropTypes.string,
/**
* If the toolbar requires splitter this property defines splitter
* index.
*/
splitterIndex: React.PropTypes.number,
/**
* Map with toolbar buttons.
*/
toolbarButtons: React.PropTypes.instanceOf(Map)
};
/**
* Constructor of Primary toolbar class.
*
* @param {Object} props - Object containing React component properties.
*/
constructor(props) {
super(props);
this._setButtonHandlers();
// Bind methods to save the context
this._renderToolbarButton = this._renderToolbarButton.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render(): ReactElement<*> {
const { className } = this.props;
return (
<div
className = { `toolbar ${className}` }
onMouseOut = { this.props._onMouseOut }
onMouseOver = { this.props._onMouseOver }>
{
[ ...this.props.toolbarButtons.entries() ]
.reduce(this._renderToolbarButton, [])
}
{
this.props.children
}
</div>
);
}
/**
* Renders toolbar button. Method is passed to reduce function.
*
* @param {Array} acc - Toolbar buttons array.
* @param {Array} keyValuePair - Key value pair containing button and its
* key.
* @param {number} index - Index of the key value pair in the array.
* @returns {Array} Array of toolbar buttons and splitter if it's on.
* @private
*/
_renderToolbarButton(acc: Array<*>, keyValuePair: Array<*>,
index: number): Array<ReactElement<*>> {
const [ key, button ] = keyValuePair;
const { splitterIndex } = this.props;
if (splitterIndex && index === splitterIndex) {
const splitter = <span className = 'toolbar__splitter' />;
acc.push(splitter);
}
const { onClick, onMount, onUnmount } = button;
acc.push(
<ToolbarButton
button = { button }
key = { key }
onClick = { onClick }
onMount = { onMount }
onUnmount = { onUnmount } />
);
return acc;
}
/**
* Sets handlers for some of the buttons.
*
* @private
* @returns {void}
*/
_setButtonHandlers(): void {
const {
buttonHandlers,
toolbarButtons
} = this.props;
Object.keys(buttonHandlers).forEach(key => {
let button = toolbarButtons.get(key);
if (button) {
button = {
...button,
...buttonHandlers[key]
};
toolbarButtons.set(key, button);
}
});
}
}
/**
* Maps part of Redux actions to component's props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @returns {Object}
* @private
*/
function _mapDispatchToProps(dispatch: Function): Object {
return {
/**
* Dispatches an action signalling that toolbar is no being hovered.
*
* @protected
* @returns {Object} Dispatched action.
*/
_onMouseOut() {
return dispatch(setToolbarHovered(false));
},
/**
* Dispatches an action signalling that toolbar is now being hovered.
*
* @protected
* @returns {Object} Dispatched action.
*/
_onMouseOver() {
return dispatch(setToolbarHovered(true));
}
};
}
export default connect(null, _mapDispatchToProps)(BaseToolbar);

View File

@ -0,0 +1,189 @@
/* @flow */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { showDesktopSharingButton, toggleFullScreen } from '../actions';
import BaseToolbar from './BaseToolbar';
import { getToolbarClassNames } from '../functions';
declare var APP: Object;
declare var interfaceConfig: Object;
/**
* Implementation of PrimaryToolbar React Component.
*
* @class PrimaryToolbar
* @extends Component
*/
class PrimaryToolbar extends Component {
state: Object;
static propTypes = {
/**
* Handler for toggling fullscreen mode.
*/
_onFullScreenToggled: React.PropTypes.func,
/**
* Handler for showing desktop sharing button.
*/
_onShowDesktopSharingButton: React.PropTypes.func,
/**
* Contains toolbar buttons for primary toolbar.
*/
_primaryToolbarButtons: React.PropTypes.instanceOf(Map),
/**
* Shows whether toolbar is visible.
*/
_visible: React.PropTypes.bool
};
/**
* Constructs instance of primary toolbar React component.
*
* @param {Object} props - React component's properties.
*/
constructor(props) {
super(props);
const buttonHandlers = {
/**
* Mount handler for desktop button.
*
* @type {Object}
*/
desktop: {
onMount: () => this.props._onShowDesktopSharingButton()
},
/**
* Mount/Unmount handler for toggling fullscreen button.
*
* @type {Object}
*/
fullscreen: {
onMount: () =>
APP.UI.addListener(UIEvents.FULLSCREEN_TOGGLED,
this.props._onFullScreenToggled),
onUnmount: () =>
APP.UI.removeListener(UIEvents.FULLSCREEN_TOGGLED,
this.props._onFullScreenToggled)
}
};
const splitterIndex = interfaceConfig.MAIN_TOOLBAR_SPLITTER_INDEX;
this.state = {
/**
* Object containing on mount/unmount handlers for toolbar buttons.
*
* @type {Object}
*/
buttonHandlers,
/**
* If deployment supports toolbar splitter this value contains its
* index.
*
* @type {number}
*/
splitterIndex
};
}
/**
* Renders primary toolbar component.
*
* @returns {ReactElement}
*/
render() {
const { buttonHandlers, splitterIndex } = this.state;
const { _primaryToolbarButtons } = this.props;
const { primaryToolbarClassName } = getToolbarClassNames(this.props);
return (
<BaseToolbar
buttonHandlers = { buttonHandlers }
className = { primaryToolbarClassName }
splitterIndex = { splitterIndex }
toolbarButtons = { _primaryToolbarButtons } />
);
}
}
/**
* Maps some of the Redux actions to the component props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @returns {{
* _onShowDesktopSharingButton: Function
* }}
* @private
*/
function _mapDispatchToProps(dispatch: Function): Object {
return {
/**
* Dispatches an action signalling that full screen mode is toggled.
*
* @param {boolean} isFullScreen - Show whether fullscreen mode is on.
* @returns {Object} Dispatched action.
*/
_onFullScreenToggled(isFullScreen: boolean) {
return dispatch(toggleFullScreen(isFullScreen));
},
/**
* Dispatches an action signalling that desktop sharing button
* should be shown.
*
* @returns {Object} Dispatched action.
*/
_onShowDesktopSharingButton() {
dispatch(showDesktopSharingButton());
}
};
}
/**
* Maps part of Redux store to React component props.
*
* @param {Object} state - Snapshot of Redux store.
* @returns {{
* _primaryToolbarButtons: Map,
* _visible: boolean
* }}
* @private
*/
function _mapStateToProps(state: Object): Object {
const {
primaryToolbarButtons,
visible
} = state['features/toolbar'];
return {
/**
* Default toolbar buttons for primary toolbar.
*
* @protected
* @type {Map}
*/
_primaryToolbarButtons: primaryToolbarButtons,
/**
* Shows whether toolbar is visible.
*
* @protected
* @type {boolean}
*/
_visible: visible
};
}
export default connect(_mapStateToProps, _mapDispatchToProps)(PrimaryToolbar);

View File

@ -0,0 +1,263 @@
/* @flow */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { FeedbackButton } from '../../feedback';
import UIEvents from '../../../../service/UI/UIEvents';
import {
changeLocalRaiseHand,
setProfileButtonUnclickable,
showRecordingButton,
toggleSideToolbarContainer
} from '../actions';
import BaseToolbar from './BaseToolbar';
import { getToolbarClassNames } from '../functions';
declare var APP: Object;
declare var config: Object;
/**
* Implementation of secondary toolbar React component.
*
* @class SecondaryToolbar
* @extends Component
*/
class SecondaryToolbar extends Component {
state: Object;
/**
* Secondary toolbar property types.
*
* @static
*/
static propTypes = {
/**
* Handler dispatching local "Raise hand".
*/
_onLocalRaiseHandChanged: React.PropTypes.func,
/**
* Handler setting profile button unclickable.
*/
_onSetProfileButtonUnclickable: React.PropTypes.func,
/**
* Handler for showing recording button.
*/
_onShowRecordingButton: React.PropTypes.func,
/**
* Handler dispatching toggle toolbar container.
*/
_onSideToolbarContainerToggled: React.PropTypes.func,
/**
* Contains map of secondary toolbar buttons.
*/
_secondaryToolbarButtons: React.PropTypes.instanceOf(Map),
/**
* Shows whether toolbar is visible.
*/
_visible: React.PropTypes.bool
};
/**
* Constructs instance of SecondaryToolbar component.
*
* @param {Object} props - React component properties.
*/
constructor(props) {
super(props);
const buttonHandlers = {
/**
* Mount handler for profile button.
*
* @type {Object}
*/
profile: {
onMount: () => {
APP.tokenData.isGuest
|| this.props._onSetProfileButtonUnclickable(true);
}
},
/**
* Mount/Unmount handlers for raisehand button.
*
* @type {button}
*/
raisehand: {
onMount: () => {
APP.UI.addListener(UIEvents.LOCAL_RAISE_HAND_CHANGED,
this.props._onLocalRaiseHandChanged);
},
onUnmount: () => {
APP.UI.removeListener(UIEvents.LOCAL_RAISE_HAND_CHANGED,
this.props._onLocalRaiseHandChanged);
}
},
/**
* Mount handler for recording button.
*
* @type {Object}
*/
recording: {
onMount: () => {
if (config.enableRecording) {
this.props._onShowRecordingButton();
}
}
}
};
this.state = {
/**
* Object containing on mount/unmount handlers for toolbar buttons.
*
* @type {Object}
*/
buttonHandlers
};
}
/**
* Register legacy UI listener.
*
* @returns {void}
*/
componentDidMount(): void {
APP.UI.addListener(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
this.props._onSideToolbarContainerToggled);
}
/**
* Unregisters legacy UI listener.
*
* @returns {void}
*/
componentWillUnmount(): void {
APP.UI.removeListener(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
this.props._onSideToolbarContainerToggled);
}
/**
* Renders secondary toolbar component.
*
* @returns {ReactElement}
*/
render(): ReactElement<*> {
const { buttonHandlers } = this.state;
const { _secondaryToolbarButtons } = this.props;
const { secondaryToolbarClassName } = getToolbarClassNames(this.props);
return (
<BaseToolbar
buttonHandlers = { buttonHandlers }
className = { secondaryToolbarClassName }
toolbarButtons = { _secondaryToolbarButtons }>
<FeedbackButton />
</BaseToolbar>
);
}
}
/**
* Maps some of Redux actions to component's props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @returns {{
* _onLocalRaiseHandChanged: Function,
* _onSetProfileButtonUnclickable: Function,
* _onShowRecordingButton: Function,
* _onSideToolbarContainerToggled
* }}
* @private
*/
function _mapDispatchToProps(dispatch: Function): Object {
return {
/**
* Dispatches an action that 'hand' is raised.
*
* @param {boolean} isRaisedHand - Show whether hand is raised.
* @returns {Object} Dispatched action.
*/
_onLocalRaiseHandChanged(isRaisedHand: boolean) {
return dispatch(changeLocalRaiseHand(isRaisedHand));
},
/**
* Dispatches an action signalling to set profile button unclickable.
*
* @param {boolean} unclickable - Flag showing whether unclickable
* property is true.
* @returns {Object} Dispatched action.
*/
_onSetProfileButtonUnclickable(unclickable: boolean) {
return dispatch(setProfileButtonUnclickable(unclickable));
},
/**
* Dispatches an action signalling that recording button should be
* shown.
*
* @returns {Object} Dispatched action.
*/
_onShowRecordingButton() {
return dispatch(showRecordingButton());
},
/**
* Dispatches an action signalling that side toolbar container is
* toggled.
*
* @param {string} containerId - Id of side toolbar container.
* @returns {Object} Dispatched action.
*/
_onSideToolbarContainerToggled(containerId: string) {
return dispatch(toggleSideToolbarContainer(containerId));
}
};
}
/**
* Maps part of Redux state to component's props.
*
* @param {Object} state - Snapshot of Redux store.
* @returns {{
* _secondaryToolbarButtons: Map,
* _visible: boolean
* }}
* @private
*/
function _mapStateToProps(state: Object): Object {
const {
secondaryToolbarButtons,
visible
} = state['features/toolbar'];
return {
/**
* Default toolbar buttons for secondary toolbar.
*
* @protected
* @type {Map}
*/
_secondaryToolbarButtons: secondaryToolbarButtons,
/**
* Shows whether toolbar is visible.
*
* @protected
* @type {boolean}
*/
_visible: visible
};
}
export default connect(_mapStateToProps, _mapDispatchToProps)(SecondaryToolbar);

View File

@ -1,41 +1,74 @@
import React from 'react'; import React, { Component } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { MEDIA_TYPE, toggleCameraFacingMode } from '../../base/media'; import { MEDIA_TYPE, toggleCameraFacingMode } from '../../base/media';
import { Container } from '../../base/react'; import { Container } from '../../base/react';
import { ColorPalette } from '../../base/styles'; import { ColorPalette } from '../../base/styles';
import { beginRoomLockRequest } from '../../room-lock';
import { AbstractToolbar, _mapStateToProps } from './AbstractToolbar'; import {
abstractMapDispatchToProps,
abstractMapStateToProps
} from '../functions';
import { styles } from './styles'; import { styles } from './styles';
import ToolbarButton from './ToolbarButton'; import ToolbarButton from './ToolbarButton';
/** /**
* Implements the conference toolbar on React Native. * Implements the conference toolbar on React Native.
*
* @extends AbstractToolbar
*/ */
class Toolbar extends AbstractToolbar { class Toolbar extends Component {
/** /**
* Toolbar component's property types. * Toolbar component's property types.
* *
* @static * @static
*/ */
static propTypes = AbstractToolbar.propTypes static propTypes = {
/**
* Flag showing that audio is muted.
*/
_audioMuted: React.PropTypes.bool,
/** /**
* Initializes a new Toolbar instance. * Flag showing whether room is locked.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/ */
constructor(props) { _locked: React.PropTypes.bool,
super(props);
// Bind event handlers so they are only bound once for every instance. /**
this._toggleCameraFacingMode * Handler for hangup.
= this._toggleCameraFacingMode.bind(this); */
} _onHangup: React.PropTypes.func,
/**
* Handler for room locking.
*/
_onRoomLock: React.PropTypes.func,
/**
* Handler for toggle audio.
*/
_onToggleAudio: React.PropTypes.func,
/**
* Handler for toggling camera facing mode.
*/
_onToggleCameraFacingMode: React.PropTypes.func,
/**
* Handler for toggling video.
*/
_onToggleVideo: React.PropTypes.func,
/**
* Flag showing whether video is muted.
*/
_videoMuted: React.PropTypes.bool,
/**
* Flag showing whether toolbar is visible.
*/
_visible: React.PropTypes.bool
};
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
@ -47,7 +80,7 @@ class Toolbar extends AbstractToolbar {
return ( return (
<Container <Container
style = { styles.toolbarContainer } style = { styles.toolbarContainer }
visible = { this.props.visible }> visible = { this.props._visible }>
{ {
this._renderPrimaryToolbar() this._renderPrimaryToolbar()
} }
@ -58,6 +91,43 @@ class Toolbar extends AbstractToolbar {
); );
} }
/**
* Gets the styles for a button that toggles the mute state of a specific
* media type.
*
* @param {string} mediaType - The {@link MEDIA_TYPE} associated with the
* button to get styles for.
* @protected
* @returns {{
* iconName: string,
* iconStyle: Object,
* style: Object
* }}
*/
_getMuteButtonStyles(mediaType) {
let iconName;
let iconStyle;
let style = styles.primaryToolbarButton;
if (this.props[`_${mediaType}Muted`]) {
iconName = this[`${mediaType}MutedIcon`];
iconStyle = styles.whiteIcon;
style = {
...style,
backgroundColor: ColorPalette.buttonUnderlay
};
} else {
iconName = this[`${mediaType}Icon`];
iconStyle = styles.icon;
}
return {
iconName,
iconStyle,
style
};
}
/** /**
* Renders the toolbar which contains the primary buttons such as hangup, * Renders the toolbar which contains the primary buttons such as hangup,
* audio and video mute. * audio and video mute.
@ -76,12 +146,12 @@ class Toolbar extends AbstractToolbar {
<ToolbarButton <ToolbarButton
iconName = { audioButtonStyles.iconName } iconName = { audioButtonStyles.iconName }
iconStyle = { audioButtonStyles.iconStyle } iconStyle = { audioButtonStyles.iconStyle }
onClick = { this._toggleAudio } onClick = { this.props._onToggleAudio }
style = { audioButtonStyles.style } /> style = { audioButtonStyles.style } />
<ToolbarButton <ToolbarButton
iconName = 'hangup' iconName = 'hangup'
iconStyle = { styles.whiteIcon } iconStyle = { styles.whiteIcon }
onClick = { this._onHangup } onClick = { this.props._onHangup }
style = {{ style = {{
...styles.primaryToolbarButton, ...styles.primaryToolbarButton,
backgroundColor: ColorPalette.red backgroundColor: ColorPalette.red
@ -90,7 +160,7 @@ class Toolbar extends AbstractToolbar {
<ToolbarButton <ToolbarButton
iconName = { videoButtonStyles.iconName } iconName = { videoButtonStyles.iconName }
iconStyle = { videoButtonStyles.iconStyle } iconStyle = { videoButtonStyles.iconStyle }
onClick = { this._toggleVideo } onClick = { this.props._onToggleVideo }
style = { videoButtonStyles.style } /> style = { videoButtonStyles.style } />
</View> </View>
); );
@ -125,7 +195,7 @@ class Toolbar extends AbstractToolbar {
<ToolbarButton <ToolbarButton
iconName = 'switch-camera' iconName = 'switch-camera'
iconStyle = { iconStyle } iconStyle = { iconStyle }
onClick = { this._toggleCameraFacingMode } onClick = { this.props._onToggleCameraFacingMode }
style = { style } style = { style }
underlayColor = { underlayColor } /> underlayColor = { underlayColor } />
*/} */}
@ -134,7 +204,7 @@ class Toolbar extends AbstractToolbar {
this.props._locked ? 'security-locked' : 'security' this.props._locked ? 'security-locked' : 'security'
} }
iconStyle = { iconStyle } iconStyle = { iconStyle }
onClick = { this._onRoomLock } onClick = { this.props._onRoomLock }
style = { style } style = { style }
underlayColor = { underlayColor } /> underlayColor = { underlayColor } />
</View> </View>
@ -142,17 +212,6 @@ class Toolbar extends AbstractToolbar {
/* eslint-enable react/jsx-curly-spacing,react/jsx-handler-names */ /* eslint-enable react/jsx-curly-spacing,react/jsx-handler-names */
} }
/**
* Switches between the front/user-facing and rear/environment-facing
* cameras.
*
* @private
* @returns {void}
*/
_toggleCameraFacingMode() {
this.props.dispatch(toggleCameraFacingMode());
}
} }
/** /**
@ -168,4 +227,70 @@ Object.assign(Toolbar.prototype, {
videoMutedIcon: 'camera-disabled' videoMutedIcon: 'camera-disabled'
}); });
export default connect(_mapStateToProps)(Toolbar); /**
* Maps actions to React component props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @returns {{
* _onRoomLock: Function,
* _onToggleCameraFacingMode: Function,
* }}
* @private
*/
function _mapDispatchToProps(dispatch) {
return {
...abstractMapDispatchToProps(dispatch),
/**
* Dispatches an action to set the lock i.e. password protection of the
* conference/room.
*
* @private
* @returns {Object} - Dispatched action.
* @type {Function}
*/
_onRoomLock() {
return dispatch(beginRoomLockRequest());
},
/**
* Switches between the front/user-facing and rear/environment-facing
* cameras.
*
* @private
* @returns {Object} - Dispatched action.
* @type {Function}
*/
_onToggleCameraFacingMode() {
return dispatch(toggleCameraFacingMode());
}
};
}
/**
* Maps part of Redux store to React component props.
*
* @param {Object} state - Redux store.
* @returns {{
* _locked: boolean
* }}
* @private
*/
function _mapStateToProps(state) {
const conference = state['features/base/conference'];
return {
...abstractMapStateToProps(state),
/**
* The indicator which determines whether the conference is
* locked/password-protected.
*
* @protected
* @type {boolean}
*/
_locked: conference.locked
};
}
export default connect(_mapStateToProps, _mapDispatchToProps)(Toolbar);

View File

@ -0,0 +1,227 @@
/* @flow */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { resetAlwaysVisibleToolbar } from '../actions';
import {
abstractMapStateToProps,
showCustomToolbarPopup
} from '../functions';
import Notice from './Notice';
import PrimaryToolbar from './PrimaryToolbar';
import SecondaryToolbar from './SecondaryToolbar';
declare var APP: Object;
declare var config: Object;
declare var interfaceConfig: Object;
/**
* Implements the conference toolbar on React.
*/
class Toolbar extends Component {
/**
* App component's property types.
*
* @static
*/
static propTypes = {
/**
* Handler dispatching reset always visible toolbar action.
*/
_onResetAlwaysVisibleToolbar: React.PropTypes.func,
/**
* Represents conference subject.
*/
_subject: React.PropTypes.string,
/**
* Flag showing whether to set subject slide in animation.
*/
_subjectSlideIn: React.PropTypes.bool,
/**
* Property containing toolbar timeout id.
*/
_timeoutId: React.PropTypes.number
};
/**
* Invokes reset always visible toolbar after mounting the component and
* registers legacy UI listeners.
*
* @returns {void}
*/
componentDidMount(): void {
this.props._onResetAlwaysVisibleToolbar();
APP.UI.addListener(UIEvents.SHOW_CUSTOM_TOOLBAR_BUTTON_POPUP,
showCustomToolbarPopup);
}
/**
* Unregisters legacy UI listeners.
*
* @returns {void}
*/
componentWillUnmount(): void {
APP.UI.removeListener(UIEvents.SHOW_CUSTOM_TOOLBAR_BUTTON_POPUP,
showCustomToolbarPopup);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render(): ReactElement<*> {
return (
<div>
{
this._renderSubject()
}
{
this._renderToolbars()
}
<div id = 'sideToolbarContainer' />
</div>
);
}
/**
* Returns React element representing toolbar subject.
*
* @returns {ReactElement}
* @private
*/
_renderSubject(): ReactElement<*> | null {
const { _subjectSlideIn, _subject } = this.props;
const classNames = [ 'subject' ];
if (!_subject) {
return null;
}
if (_subjectSlideIn) {
classNames.push('subject_slide-in');
} else {
classNames.push('subject_slide-out');
}
// XXX: Since chat is now not reactified we have to dangerously set
// inner HTML into the component. This has to be refactored while
// reactification of the Chat.js
const innerHtml = {
__html: _subject
};
return (
<div
className = { classNames.join(' ') }
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML = { innerHtml }
id = 'subject' />
);
}
/**
* Renders primary and secondary toolbars.
*
* @returns {ReactElement}
* @private
*/
_renderToolbars(): ReactElement<*> | null {
// We should not show the toolbars till timeout object will be
// initialized.
if (this.props._timeoutId === null) {
return null;
}
return (
<div>
<Notice />
<PrimaryToolbar />
<SecondaryToolbar />
</div>
);
}
}
/**
* Maps parts of Redux actions to component props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @returns {{
* _onResetAlwaysVisibleToolbar: Function
* }}
* @private
*/
function _mapDispatchToProps(dispatch: Function): Object {
return {
/**
* Dispatches an action resetting always visible toolbar.
*
* @returns {Object} Dispatched action.
*/
_onResetAlwaysVisibleToolbar() {
dispatch(resetAlwaysVisibleToolbar());
}
};
}
/**
* Maps parts of toolbar state to component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {{
* _audioMuted: boolean,
* _locked: boolean,
* _subjectSlideIn: boolean,
* _videoMuted: boolean
* }}
*/
function _mapStateToProps(state: Object): Object {
const {
subject,
subjectSlideIn,
timeoutId
} = state['features/toolbar'];
return {
...abstractMapStateToProps(state),
/**
* Property containing conference subject.
*
* @protected
* @type {string}
*/
_subject: subject,
/**
* Flag showing whether to set subject slide in animation.
*
* @protected
* @type {boolean}
*/
_subjectSlideIn: subjectSlideIn,
/**
* Property containing toolbar timeout id.
*
* @protected
* @type {number}
*/
_timeoutId: timeoutId
};
}
export default connect(_mapStateToProps, _mapDispatchToProps)(Toolbar);

View File

@ -0,0 +1,228 @@
/* @flow */
import React from 'react';
import { translate } from '../../base/i18n';
import UIUtil from '../../../../modules/UI/util/UIUtil';
import AbstractToolbarButton from './AbstractToolbarButton';
import { getButtonAttributesByProps } from '../functions';
declare var APP: Object;
declare var interfaceConfig: Object;
/**
* Represents a button in Toolbar on React.
*
* @class ToolbarButton
* @extends AbstractToolbarButton
*/
class ToolbarButton extends AbstractToolbarButton {
_createRefToButton: Function;
_onClick: Function;
/**
* Toolbar button component's property types.
*
* @static
*/
static propTypes = {
...AbstractToolbarButton.propTypes,
/**
* Object describing button.
*/
button: React.PropTypes.object.isRequired,
/**
* Handler for component mount.
*/
onMount: React.PropTypes.func,
/**
* Handler for component unmount.
*/
onUnmount: React.PropTypes.func,
/**
* Translation helper function.
*/
t: React.PropTypes.func
};
/**
* Initializes new ToolbarButton instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Object) {
super(props);
// Bind methods to save the context
this._createRefToButton = this._createRefToButton.bind(this);
this._onClick = this._onClick.bind(this);
}
/**
* Sets shortcut/tooltip
* after mounting of the component.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount(): void {
this._setShortcutAndTooltip();
if (this.props.onMount) {
this.props.onMount();
}
}
/**
* Invokes on unmount handler if it was passed to the props.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount(): void {
if (this.props.onUnmount) {
this.props.onUnmount();
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render(): ReactElement<*> {
const { button } = this.props;
const attributes = getButtonAttributesByProps(button);
const popups = button.popups || [];
return (
<a
{ ...attributes }
onClick = { this._onClick }
ref = { this._createRefToButton }>
{ this._renderInnerElementsIfRequired() }
{ this._renderPopups(popups) }
</a>
);
}
/**
* Creates reference to current toolbar button.
*
* @param {HTMLElement} element - HTMLElement representing the toolbar
* button.
* @returns {void}
* @private
*/
_createRefToButton(element: HTMLElement): void {
this.button = element;
}
/**
* Wrapper on on click handler props for current button.
*
* @param {Event} event - Click event object.
* @returns {void}
* @private
*/
_onClick(event: Event): void {
const {
button,
onClick
} = this.props;
const {
enabled,
unclickable
} = button;
if (enabled && !unclickable && onClick) {
onClick(event);
}
}
/**
* If toolbar button should contain children elements
* renders them.
*
* @returns {ReactElement|null}
* @private
*/
_renderInnerElementsIfRequired(): ReactElement<*> | null {
if (this.props.button.html) {
return this.props.button.html;
}
return null;
}
/**
* Renders popup element for toolbar button.
*
* @param {Array} popups - Array of popup objects.
* @returns {Array}
* @private
*/
_renderPopups(popups: Array<*> = []): Array<*> {
return popups.map(popup => {
let gravity = 'n';
if (popup.dataAttrPosition) {
gravity = popup.dataAttrPosition;
}
const title = this.props.t(popup.dataAttr);
return (
<div
className = { popup.className }
data-popup = { gravity }
id = { popup.id }
key = { popup.id }
title = { title } />
);
});
}
/**
* Sets shortcut and tooltip for current toolbar button.
*
* @private
* @returns {void}
*/
_setShortcutAndTooltip(): void {
const { button } = this.props;
const name = button.buttonName;
if (UIUtil.isButtonEnabled(name)) {
const tooltipPosition
= interfaceConfig.MAIN_TOOLBAR_BUTTONS.indexOf(name) > -1
? 'bottom' : 'right';
if (!button.unclickable) {
UIUtil.setTooltip(this.button,
button.tooltipKey,
tooltipPosition);
}
if (button.shortcut) {
APP.keyboardshortcut.registerShortcut(
button.shortcut,
button.shortcutAttr,
button.shortcutFunc,
button.shortcutDescription
);
}
}
}
}
export default translate(ToolbarButton);

View File

@ -1,2 +1 @@
export { default as Notice } from './Notice';
export { default as Toolbar } from './Toolbar'; export { default as Toolbar } from './Toolbar';

View File

@ -0,0 +1,389 @@
/* @flow */
import React from 'react';
import UIEvents from '../../../service/UI/UIEvents';
declare var APP: Object;
declare var config: Object;
declare var JitsiMeetJS: Object;
/**
* Shows SIP number dialog.
*
* @returns {void}
*/
function _showSIPNumberInput() {
const defaultNumber = config.defaultSipNumber || '';
const msgString
= `<input class="input-control" name="sipNumber" type="text" value="${
defaultNumber}" autofocus>`;
APP.UI.messageHandler.openTwoButtonDialog({
focus: ':input:first',
leftButtonKey: 'dialog.Dial',
msgString,
titleKey: 'dialog.sipMsg',
// eslint-disable-next-line max-params
submitFunction(event, value, message, formValues) {
const { sipNumber } = formValues;
if (value && sipNumber) {
APP.UI.emitEvent(UIEvents.SIP_DIAL, sipNumber);
}
}
});
}
/**
* All toolbar buttons' descriptions.
*/
export default {
/**
* Object representing camera button.
*/
camera: {
classNames: [ 'button', 'icon-camera' ],
enabled: true,
id: 'toolbar_button_camera',
onClick() {
if (APP.conference.videoMuted) {
JitsiMeetJS.analytics.sendEvent('toolbar.video.enabled');
APP.UI.emitEvent(UIEvents.VIDEO_MUTED, false);
} else {
JitsiMeetJS.analytics.sendEvent('toolbar.video.disabled');
APP.UI.emitEvent(UIEvents.VIDEO_MUTED, true);
}
},
shortcut: 'V',
shortcutAttr: 'toggleVideoPopover',
shortcutFunc() {
JitsiMeetJS.analytics.sendEvent('shortcut.videomute.toggled');
APP.conference.toggleVideoMuted();
},
shortcutDescription: 'keyboardShortcuts.videoMute',
tooltipKey: 'toolbar.videomute'
},
/**
* The Object representing chat button.
*/
chat: {
classNames: [ 'button', 'icon-chat' ],
enabled: true,
html: <span className = 'badge-round'>
<span id = 'unreadMessages' />
</span>,
id: 'toolbar_button_chat',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.chat.toggled');
APP.UI.emitEvent(UIEvents.TOGGLE_CHAT);
},
shortcut: 'C',
shortcutAttr: 'toggleChatPopover',
shortcutFunc() {
JitsiMeetJS.analytics.sendEvent('shortcut.chat.toggled');
APP.UI.toggleChat();
},
shortcutDescription: 'keyboardShortcuts.toggleChat',
sideContainerId: 'chat_container',
tooltipKey: 'toolbar.chat'
},
/**
* The object representing contact list button.
*/
contacts: {
classNames: [ 'button', 'icon-contactList' ],
enabled: true,
// XXX: Hotfix to solve race condition between toolbar rendering and
// contact list view that updates the number of active participants
// via jQuery. There is case when contact list view updates number of
// participants but toolbar has not been rendered yet. Since this issue
// is reproducible only for conferences with the only participant let's
// use 1 participant as a default value for this badge. Later after
// reactification of contact list let's use the value of active
// paricipants from Redux store.
html: <span className = 'badge-round'>
<span id = 'numberOfParticipants'>1</span>
</span>,
id: 'toolbar_contact_list',
onClick() {
JitsiMeetJS.analytics.sendEvent(
'toolbar.contacts.toggled');
APP.UI.emitEvent(UIEvents.TOGGLE_CONTACT_LIST);
},
sideContainerId: 'contacts_container',
tooltipKey: 'bottomtoolbar.contactlist'
},
/**
* The object representing desktop sharing button.
*/
desktop: {
classNames: [ 'button', 'icon-share-desktop' ],
enabled: true,
id: 'toolbar_button_desktopsharing',
onClick() {
if (APP.conference.isSharingScreen) {
JitsiMeetJS.analytics.sendEvent('toolbar.screen.disabled');
} else {
JitsiMeetJS.analytics.sendEvent('toolbar.screen.enabled');
}
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
},
shortcut: 'D',
shortcutAttr: 'toggleDesktopSharingPopover',
shortcutFunc() {
JitsiMeetJS.analytics.sendEvent('shortcut.screen.toggled');
APP.conference.toggleScreenSharing();
},
shortcutDescription: 'keyboardShortcuts.toggleScreensharing',
tooltipKey: 'toolbar.sharescreen'
},
/**
* The object representing dialpad button.
*/
dialpad: {
classNames: [ 'button', 'icon-dialpad' ],
enabled: true,
// TODO: remove it after UI.updateDTMFSupport fix
hidden: true,
id: 'toolbar_button_dialpad',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.sip.dialpad.clicked');
},
tooltipKey: 'toolbar.dialpad'
},
/**
* The object representing etherpad button.
*/
etherpad: {
classNames: [ 'button', 'icon-share-doc' ],
enabled: true,
hidden: true,
id: 'toolbar_button_etherpad',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.etherpad.clicked');
APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED);
},
tooltipKey: 'toolbar.etherpad'
},
/**
* The object representing button toggling full screen mode.
*/
fullscreen: {
classNames: [ 'button', 'icon-full-screen' ],
enabled: true,
id: 'toolbar_button_fullScreen',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.fullscreen.enabled');
APP.UI.emitEvent(UIEvents.TOGGLE_FULLSCREEN);
},
shortcut: 'S',
shortcutAttr: 'toggleFullscreenPopover',
shortcutDescription: 'keyboardShortcuts.fullScreen',
shortcutFunc() {
JitsiMeetJS.analytics.sendEvent('shortcut.fullscreen.toggled');
APP.UI.toggleFullScreen();
},
tooltipKey: 'toolbar.fullscreen'
},
/**
* The object representing hanging the call up button.
*/
hangup: {
classNames: [ 'button', 'icon-hangup', 'button_hangup' ],
enabled: true,
id: 'toolbar_button_hangup',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.hangup');
APP.UI.emitEvent(UIEvents.HANGUP);
},
tooltipKey: 'toolbar.hangup'
},
/**
* The object representing button showing invite user dialog.
*/
invite: {
classNames: [ 'button', 'icon-link' ],
enabled: true,
id: 'toolbar_button_link',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.invite.clicked');
APP.UI.emitEvent(UIEvents.INVITE_CLICKED);
},
tooltipKey: 'toolbar.invite'
},
/**
* The object representing microphone button.
*/
microphone: {
classNames: [ 'button', 'icon-microphone' ],
enabled: true,
id: 'toolbar_button_mute',
onClick() {
const sharedVideoManager = APP.UI.getSharedVideoManager();
if (APP.conference.audioMuted) {
// If there's a shared video with the volume "on" and we aren't
// the video owner, we warn the user
// that currently it's not possible to unmute.
if (sharedVideoManager
&& sharedVideoManager.isSharedVideoVolumeOn()
&& !sharedVideoManager.isSharedVideoOwner()) {
APP.UI.showCustomToolbarPopup(
'#unableToUnmutePopup', true, 5000);
} else {
JitsiMeetJS.analytics.sendEvent('toolbar.audio.unmuted');
APP.UI.emitEvent(UIEvents.AUDIO_MUTED, false, true);
}
} else {
JitsiMeetJS.analytics.sendEvent('toolbar.audio.muted');
APP.UI.emitEvent(UIEvents.AUDIO_MUTED, true, true);
}
},
popups: [
{
className: 'loginmenu',
dataAttr: 'toolbar.micMutedPopup',
id: 'micMutedPopup'
},
{
className: 'loginmenu',
dataAttr: 'toolbar.unableToUnmutePopup',
id: 'unableToUnmutePopup'
},
{
className: 'loginmenu',
dataAttr: 'toolbar.talkWhileMutedPopup',
id: 'talkWhileMutedPopup'
}
],
shortcut: 'M',
shortcutAttr: 'mutePopover',
shortcutFunc() {
JitsiMeetJS.analytics.sendEvent('shortcut.audiomute.toggled');
APP.conference.toggleAudioMuted();
},
shortcutDescription: 'keyboardShortcuts.mute',
tooltipKey: 'toolbar.mute'
},
/**
* The object representing profile button.
*/
profile: {
classNames: [ 'button' ],
enabled: true,
html: <img
id = 'avatar'
src = 'images/avatar2.png' />,
id: 'toolbar_button_profile',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.profile.toggled');
APP.UI.emitEvent(UIEvents.TOGGLE_PROFILE);
},
sideContainerId: 'profile_container',
tooltipKey: 'profile.setDisplayNameLabel'
},
/**
* The object representing "Raise hand" button.
*/
raisehand: {
classNames: [ 'button', 'icon-raised-hand' ],
enabled: true,
id: 'toolbar_button_raisehand',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.raiseHand.clicked');
APP.conference.maybeToggleRaisedHand();
},
shortcut: 'R',
shortcutAttr: 'raiseHandPopover',
shortcutDescription: 'keyboardShortcuts.raiseHand',
shortcutFunc() {
JitsiMeetJS.analytics.sendEvent('shortcut.raisehand.clicked');
APP.conference.maybeToggleRaisedHand();
},
tooltipKey: 'toolbar.raiseHand'
},
/**
* The object representing recording button. Requires additional
* initialization in Recording module.
*/
recording: {
classNames: [ 'button' ],
enabled: true,
// will be displayed once the recording functionality is detected
hidden: true,
id: 'toolbar_button_record',
tooltipKey: 'liveStreaming.buttonTooltip'
},
/**
* The objecr representing settings button.
*/
settings: {
classNames: [ 'button', 'icon-settings' ],
enabled: true,
id: 'toolbar_button_settings',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.settings.toggled');
APP.UI.emitEvent(UIEvents.TOGGLE_SETTINGS);
},
sideContainerId: 'settings_container',
tooltipKey: 'toolbar.Settings'
},
/**
* The object representing sharing Youtube video button.
*/
sharedvideo: {
classNames: [ 'button', 'icon-shared-video' ],
enabled: true,
id: 'toolbar_button_sharedvideo',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.sharedvideo.clicked');
APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
},
popups: [
{
className: 'loginmenu extendedToolbarPopup',
dataAttr: 'toolbar.sharedVideoMutedPopup',
dataAttrPosition: 'w',
id: 'sharedVideoMutedPopup'
}
],
tooltipKey: 'toolbar.sharedvideo'
},
/**
* The object representing SIP call.
*/
sip: {
classNames: [ 'button', 'icon-telephone' ],
enabled: true,
// Will be displayed once the SIP calls functionality is detected.
hidden: true,
id: 'toolbar_button_sip',
onClick() {
JitsiMeetJS.analytics.sendEvent('toolbar.sip.clicked');
_showSIPNumberInput();
},
tooltipKey: 'toolbar.sip'
}
};

View File

@ -0,0 +1,254 @@
/* @flow */
import SideContainerToggler
from '../../../modules/UI/side_pannels/SideContainerToggler';
import { appNavigate } from '../app';
import { toggleAudioMuted, toggleVideoMuted } from '../base/media';
import defaultToolbarButtons from './defaultToolbarButtons';
declare var $: Function;
declare var AJS: Object;
declare var interfaceConfig: Object;
import type { Dispatch } from 'redux-thunk';
type MapOfAttributes = { [key: string]: * };
/**
* Maps actions to React component props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @returns {{
* _onHangup: Function,
* _onToggleAudio: Function,
* _onToggleVideo: Function
* }}
* @private
*/
export function abstractMapDispatchToProps(dispatch: Dispatch<*>): Object {
return {
/**
* Dispatches action to leave the current conference.
*
* @private
* @returns {void}
* @type {Function}
*/
_onHangup() {
// XXX We don't know here which value is effectively/internally
// used when there's no valid room name to join. It isn't our
// business to know that anyway. The undefined value is our
// expression of (1) the lack of knowledge & (2) the desire to no
// longer have a valid room name to join.
return dispatch(appNavigate(undefined));
},
/**
* Dispatches an action to toggle the mute state of the
* audio/microphone.
*
* @private
* @returns {Object} - Dispatched action.
* @type {Function}
*/
_onToggleAudio() {
return dispatch(toggleAudioMuted());
},
/**
* Dispatches an action to toggle the mute state of the video/camera.
*
* @private
* @returns {Object} - Dispatched action.
* @type {Function}
*/
_onToggleVideo() {
return dispatch(toggleVideoMuted());
}
};
}
/**
* Maps parts of media state to component props.
*
* @param {Object} state - Redux state.
* @protected
* @returns {{
* _audioMuted: boolean,
* _videoMuted: boolean,
* _visible: boolean
* }}
*/
export function abstractMapStateToProps(state: Object): Object {
const media = state['features/base/media'];
const { visible } = state['features/toolbar'];
return {
/**
* Flag showing that audio is muted.
*
* @protected
* @type {boolean}
*/
_audioMuted: media.audio.muted,
/**
* Flag showing whether video is muted.
*
* @protected
* @type {boolean}
*/
_videoMuted: media.video.muted,
/**
* Flag showing whether toolbar is visible.
*
* @protected
* @type {boolean}
*/
_visible: visible
};
}
/**
* Takes toolbar button props and maps them to HTML attributes to set.
*
* @param {Object} props - Props set to the React component.
* @returns {Object}
*/
export function getButtonAttributesByProps(props: Object): MapOfAttributes {
const classNames = [ ...props.classNames ];
props.toggled && classNames.push('toggled');
props.unclickable && classNames.push('unclickable');
const result: MapOfAttributes = {
className: classNames.join(' '),
'data-container': 'body',
'data-placement': 'bottom',
id: props.id
};
if (!props.enabled) {
result.disabled = 'disabled';
}
if (props.hidden) {
result.style = { display: 'none' };
}
return result;
}
/**
* Returns object containing default buttons for the primary and secondary
* toolbars.
*
* @returns {Object}
*/
export function getDefaultToolbarButtons(): Object {
let toolbarButtons = {
primaryToolbarButtons: new Map(),
secondaryToolbarButtons: new Map()
};
if (typeof interfaceConfig !== 'undefined'
&& interfaceConfig.TOOLBAR_BUTTONS) {
toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS.reduce(
(acc, buttonName) => {
const button = defaultToolbarButtons[buttonName];
if (button) {
const place = _getToolbarButtonPlace(buttonName);
button.buttonName = buttonName;
acc[place].set(buttonName, button);
}
return acc;
}, toolbarButtons);
}
return toolbarButtons;
}
/**
* Get place for toolbar button.
* Now it can be in main toolbar or in extended (left) toolbar.
*
* @param {string} btn - Button name.
* @private
* @returns {string}
*/
function _getToolbarButtonPlace(btn) {
return (
interfaceConfig.MAIN_TOOLBAR_BUTTONS.includes(btn)
? 'primaryToolbarButtons'
: 'secondaryToolbarButtons');
}
/**
* Returns toolbar class names to add while rendering.
*
* @param {Object} props - Props object pass to React component.
* @returns {Object}
* @private
*/
export function getToolbarClassNames(props: Object) {
const primaryToolbarClassNames = [ 'toolbar_primary' ];
const secondaryToolbarClassNames = [ 'toolbar_secondary' ];
if (props._visible) {
const slideInAnimation
= SideContainerToggler.isVisible ? 'slideInExtX' : 'slideInX';
primaryToolbarClassNames.push('fadeIn');
secondaryToolbarClassNames.push(slideInAnimation);
} else {
const slideOutAnimation
= SideContainerToggler.isVisible ? 'slideOutExtX' : 'slideOutX';
primaryToolbarClassNames.push('fadeOut');
secondaryToolbarClassNames.push(slideOutAnimation);
}
return {
primaryToolbarClassName: primaryToolbarClassNames.join(' '),
secondaryToolbarClassName: secondaryToolbarClassNames.join(' ')
};
}
/**
* Show custom popup/tooltip for a specified button.
*
* @param {string} popupSelectorID - The selector id of the popup to show.
* @param {boolean} show - True or false/show or hide the popup.
* @param {number} timeout - The time to show the popup.
* @returns {void}
*/
export function showCustomToolbarPopup(
popupSelectorID: string,
show: boolean,
timeout: number) {
AJS.$(popupSelectorID).tooltip({
gravity: $(popupSelectorID).attr('data-popup'),
html: true,
title: 'title',
trigger: 'manual'
});
if (show) {
AJS.$(popupSelectorID).tooltip('show');
setTimeout(
() => {
// hide the tooltip
AJS.$(popupSelectorID).tooltip('hide');
},
timeout);
} else {
AJS.$(popupSelectorID).tooltip('hide');
}
}

View File

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

View File

@ -0,0 +1,39 @@
/* @flow */
import { MiddlewareRegistry } from '../base/redux';
import {
CLEAR_TOOLBAR_TIMEOUT,
SET_TOOLBAR_TIMEOUT
} from './actionTypes';
/**
* Middleware that captures toolbar actions and handle changes in toolbar
* timeout.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CLEAR_TOOLBAR_TIMEOUT: {
const { timeoutId } = store.getState()['features/toolbar'];
clearTimeout(timeoutId);
break;
}
case SET_TOOLBAR_TIMEOUT: {
const { timeoutId } = store.getState()['features/toolbar'];
const { handler, toolbarTimeout } = action;
clearTimeout(timeoutId);
const newTimeoutId = setTimeout(handler, toolbarTimeout);
action.timeoutId = newTimeoutId;
break;
}
}
return next(action);
});

View File

@ -0,0 +1,191 @@
/* @flow */
import { ReducerRegistry } from '../base/redux';
import {
CLEAR_TOOLBAR_TIMEOUT,
SET_ALWAYS_VISIBLE_TOOLBAR,
SET_SUBJECT,
SET_SUBJECT_SLIDE_IN,
SET_TOOLBAR_BUTTON,
SET_TOOLBAR_HOVERED,
SET_TOOLBAR_TIMEOUT,
SET_TOOLBAR_TIMEOUT_NUMBER,
SET_TOOLBAR_VISIBLE
} from './actionTypes';
import { getDefaultToolbarButtons } from './functions';
declare var interfaceConfig: Object;
/**
* Returns initial state for toolbar's part of Redux store.
*
* @returns {{
* primaryToolbarButtons: Map,
* secondaryToolbarButtons: Map
* }}
* @private
*/
function _getInitialState() {
// Default toolbar timeout for mobile app.
let toolbarTimeout = 5000;
if (typeof interfaceConfig !== 'undefined'
&& interfaceConfig.INITIAL_TOOLBAR_TIMEOUT) {
toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
}
return {
/**
* Contains default toolbar buttons for primary and secondary toolbars.
*
* @type {Map}
*/
...getDefaultToolbarButtons(),
/**
* Shows whether toolbar is always visible.
*
* @type {boolean}
*/
alwaysVisible: false,
/**
* Shows whether toolbar is hovered.
*
* @type {boolean}
*/
hovered: false,
/**
* Contains text of conference subject.
*
* @type {string}
*/
subject: '',
/**
* Shows whether subject is sliding in.
*
* @type {boolean}
*/
subjectSlideIn: false,
/**
* Contains toolbar timeout id.
*
* @type {number|null}
*/
timeoutId: null,
/**
* Contains delay of toolbar timeout.
*
* @type {number}
*/
toolbarTimeout,
/**
* Shows whether toolbar is visible.
*
* @type {boolean}
*/
visible: false
};
}
ReducerRegistry.register(
'features/toolbar',
(state: Object = _getInitialState(), action: Object) => {
switch (action.type) {
case CLEAR_TOOLBAR_TIMEOUT:
return {
...state,
timeoutId: undefined
};
case SET_ALWAYS_VISIBLE_TOOLBAR:
return {
...state,
alwaysVisible: action.alwaysVisible
};
case SET_SUBJECT:
return {
...state,
subject: action.subject
};
case SET_SUBJECT_SLIDE_IN:
return {
...state,
subjectSlideIn: action.subjectSlideIn
};
case SET_TOOLBAR_BUTTON:
return _setButton(state, action);
case SET_TOOLBAR_HOVERED:
return {
...state,
hovered: action.hovered
};
case SET_TOOLBAR_TIMEOUT:
return {
...state,
toolbarTimeout: action.toolbarTimeout,
timeoutId: action.timeoutId
};
case SET_TOOLBAR_TIMEOUT_NUMBER:
return {
...state,
toolbarTimeout: action.toolbarTimeout
};
case SET_TOOLBAR_VISIBLE:
return {
...state,
visible: action.visible
};
}
return state;
});
/**
* Sets new value of the button.
*
* @param {Object} state - Redux state.
* @param {Object} action - Dispatched action.
* @param {Object} action.button - Object describing toolbar button.
* @param {Object} action.buttonName - The name of the button.
* @returns {Object}
* @private
*/
function _setButton(state, { buttonName, button }): Object {
const {
primaryToolbarButtons,
secondaryToolbarButtons
} = state;
let selectedButton = primaryToolbarButtons.get(buttonName);
let place = 'primaryToolbarButtons';
if (!selectedButton) {
selectedButton = secondaryToolbarButtons.get(buttonName);
place = 'secondaryToolbarButtons';
}
selectedButton = {
...selectedButton,
...button
};
const updatedToolbar = state[place].set(buttonName, selectedButton);
return {
...state,
[place]: new Map(updatedToolbar)
};
}