From da4425b5c0eb8ec06dfa9d350c15a27460f3dec2 Mon Sep 17 00:00:00 2001 From: Ilya Daynatovich Date: Thu, 16 Feb 2017 17:02:40 -0600 Subject: [PATCH] React Toolbar --- conference.js | 5 + css/_animations.scss | 14 - css/_notice.scss | 5 +- css/_toolbars.scss | 392 ++++---- css/_variables.scss | 13 +- modules/UI/UI.js | 85 +- modules/UI/recording/Recording.js | 5 +- modules/UI/shared_video/SharedVideo.js | 9 +- modules/UI/side_pannels/chat/Chat.js | 12 +- modules/UI/toolbars/Toolbar.js | 889 ------------------ modules/UI/toolbars/ToolbarToggler.js | 149 --- .../components/Conference.native.js | 129 ++- .../conference/components/Conference.web.js | 23 +- .../film-strip/components/FilmStrip.js | 21 +- react/features/toolbar/actionTypes.js | 98 ++ react/features/toolbar/actions.native.js | 277 ++++++ react/features/toolbar/actions.web.js | 291 ++++++ .../toolbar/components/AbstractToolbar.js | 165 ---- .../toolbar/components/BaseToolbar.web.js | 200 ++++ .../toolbar/components/PrimaryToolbar.web.js | 189 ++++ .../components/SecondaryToolbar.web.js | 263 ++++++ .../toolbar/components/Toolbar.native.js | 197 +++- .../toolbar/components/Toolbar.web.js | 227 +++++ .../toolbar/components/ToolbarButton.web.js | 228 +++++ react/features/toolbar/components/index.js | 1 - .../features/toolbar/defaultToolbarButtons.js | 389 ++++++++ react/features/toolbar/functions.js | 254 +++++ react/features/toolbar/index.js | 5 + react/features/toolbar/middleware.js | 39 + react/features/toolbar/reducer.js | 191 ++++ 30 files changed, 3246 insertions(+), 1519 deletions(-) delete mode 100644 modules/UI/toolbars/Toolbar.js delete mode 100644 modules/UI/toolbars/ToolbarToggler.js create mode 100644 react/features/toolbar/actionTypes.js create mode 100644 react/features/toolbar/actions.native.js create mode 100644 react/features/toolbar/actions.web.js delete mode 100644 react/features/toolbar/components/AbstractToolbar.js create mode 100644 react/features/toolbar/components/BaseToolbar.web.js create mode 100644 react/features/toolbar/components/PrimaryToolbar.web.js create mode 100644 react/features/toolbar/components/SecondaryToolbar.web.js create mode 100644 react/features/toolbar/components/ToolbarButton.web.js create mode 100644 react/features/toolbar/defaultToolbarButtons.js create mode 100644 react/features/toolbar/functions.js create mode 100644 react/features/toolbar/middleware.js create mode 100644 react/features/toolbar/reducer.js diff --git a/conference.js b/conference.js index 4509db382..2ecdb1bdf 100644 --- a/conference.js +++ b/conference.js @@ -20,6 +20,8 @@ import analytics from './modules/analytics/analytics'; import EventEmitter from "events"; +import { showDesktopSharingButton } from './react/features/toolbar'; + import { AVATAR_ID_COMMAND, AVATAR_URL_COMMAND, @@ -583,6 +585,9 @@ export default { APP.connection = connection = con; this.isDesktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled(); + + APP.store.dispatch(showDesktopSharingButton()); + APP.remoteControl.init(); this._createRoom(tracks); diff --git a/css/_animations.scss b/css/_animations.scss index c01676b69..506f1ec4f 100644 --- a/css/_animations.scss +++ b/css/_animations.scss @@ -66,18 +66,4 @@ @include keyframes(slideInExtContainer) { from { width: 0; } 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; } } \ No newline at end of file diff --git a/css/_notice.scss b/css/_notice.scss index 86085bb5d..cbd6c5159 100644 --- a/css/_notice.scss +++ b/css/_notice.scss @@ -1,8 +1,11 @@ .notice { - position: relative; + position: absolute; + left: 50%; z-index: $zindex3; margin-top: 6px; + @include transform(translateX(-50%)); + &__message { background-color: #000000; color: white; diff --git a/css/_toolbars.scss b/css/_toolbars.scss index 4d9a9b80a..5ae590ea3 100644 --- a/css/_toolbars.scss +++ b/css/_toolbars.scss @@ -1,184 +1,234 @@ -.toolbar { - background-color: $toolbarBackground; - position: relative; - z-index: $toolbarZ; - height: 100%; - pointer-events: auto; - - /** - * Splitter button in the toolbar. - */ - &__splitter { - display: inline-block; - vertical-align: middle; - width: 1px; - height: 50%; - margin: 0 $splitterToolbarButtonMargin; - background: $splitterColor; - } -} - -#mainToolbarContainer{ - display: block; - position: absolute; - text-align: center; - 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; - margin-left: auto; - margin-right: auto; - width: auto; - border-radius: 3px; - .button:first-child { - border-bottom-left-radius: 3px; - border-top-left-radius: 3px; - } - .button:last-child { - border-bottom-right-radius: 3px; - border-top-right-radius: 3px; - } -} - -#extendedToolbar { - display: -moz-box; - display: -ms-flexbox; - display: -webkit-box; - display: -webkit-flex; - display: flex; - width: $defaultToolbarSize; - height: 100%; - top: 0; - left: 0; - padding-top: 10px; - box-sizing: border-box; - flex-direction: column; - flex-wrap: nowrap; - justify-content: flex-start; - align-items: center; - transform: translateX(-100%); - -webkit-transform: translateX(-100%); -} - -#toolbar_button_hangup { - color: #BF2117; - 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; - text-decoration: none; - // sum opacity with background layer should give us 0.8 - background: $toolbarSelectBackground; -} - -a.button>#avatar { - width: 30px; - border-radius: 50%; - padding-top: 10px; - padding-bottom: 10px; -} - -#feedbackButton { - margin-top: auto; -} - /** * 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; + 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 specific round badge. - */ -.toolbar .badge-round { +* 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: 9px; - bottom: 9px; + right:0; + text-align: center; + top:0; + z-index: $toolbarZ; +} + +/** +* Common toolbar styles. +*/ +.toolbar { + background-color: $toolbarBackground; + height: 100%; + pointer-events: auto; + position: relative; + z-index: $toolbarZ; + + /** + * Splitter button in the toolbar. + */ + &__splitter { + background: $splitterColor; + display: inline-block; + height: 50%; + margin: 0 $splitterToolbarButtonMargin; + vertical-align: middle; + width: 1px; + } + + /** + * Primary toolbar styles. + */ + &_primary { + position: absolute; + left: 50%; + top: 30px; + display: inline-block; + width: auto; + height: $defaultToolbarSize; + border-radius: 3px; + opacity: 0; + + @include transform(translateX(-50%)); + + .button:first-child { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; + } + .button:last-child { + border-bottom-right-radius: 3px; + border-top-right-radius: 3px; + } + } + + &_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: -ms-flexbox; + display: -webkit-box; + display: -webkit-flex; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + height: 100%; + justify-content: flex-start; + left: 0; + padding-top: 10px; + top: 0; + transform: translateX(-100%); + width: $defaultToolbarSize; + -webkit-transform: translateX(-100%); + + .button.toggled:not(.icon-raised-hand) { + background: $toolbarSelectBackground; + cursor: pointer; + text-decoration: none; + + &.unclickable { + cursor: default; + + &:hover, &:active, &.selected { + background: none; + cursor: default; + } + } + } + } + + /** + * Toolbar specific round badge. + */ + .badge-round { + bottom: 9px; + position: absolute; + right: 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 */ .fadeIn { - @include animation('fadeIn .3s linear .2s forwards'); + opacity: 1; + + @include transition(all .3s ease-in); } .fadeOut { - @include animation('fadeOut .5s linear forwards'); + opacity: 0; + + @include transition(all .3s ease-out); } diff --git a/css/_variables.scss b/css/_variables.scss index 2061601b1..e6580fb4a 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -4,13 +4,12 @@ * Style variables */ $baseFontFamily: 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif; -$toolbarFontSize: 1.9em; +$hangupColor: #bf2117; $hangupFontSize: 2em; /** * Size variables. */ -$defaultToolbarSize: 50px; // Video layout. $thumbnailToolbarHeight: 22px; @@ -34,14 +33,16 @@ $tooltipBg: rgba(0,0,0, 0.7); /** * Toolbar */ -$toolbarTitleColor: #FFFFFF; -$toolbarTitleFontSize: 19px; +$defaultToolbarSize: 50px; +$splitterToolbarButtonMargin: 18px; $toolbarBackground: rgba(0, 0, 0, 0.5); -$toolbarSelectBackground: rgba(0, 0, 0, .6); $toolbarBadgeBackground: #165ECC; $toolbarBadgeColor: #FFFFFF; +$toolbarFontSize: 1.9em; +$toolbarSelectBackground: rgba(0, 0, 0, .6); +$toolbarTitleColor: #FFFFFF; +$toolbarTitleFontSize: 19px; $toolbarToggleBackground: #12499C; -$splitterToolbarButtonMargin: 18px; /** * Main controls diff --git a/modules/UI/UI.js b/modules/UI/UI.js index d5624c4bf..3a13d2e56 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -6,8 +6,6 @@ var UI = {}; import Chat from "./side_pannels/chat/Chat"; import SidePanels from "./side_pannels/SidePanels"; -import Toolbar from "./toolbars/Toolbar"; -import ToolbarToggler from "./toolbars/ToolbarToggler"; import Avatar from "./avatar/Avatar"; import SideContainerToggler from "./side_pannels/SideContainerToggler"; import UIUtil from "./util/UIUtil"; @@ -25,6 +23,23 @@ import RingOverlay from "./ring_overlay/RingOverlay"; import UIErrors from './UIErrors'; 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"); UI.messageHandler = require("./util/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] = "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 * (a.k.a. presentation mode in Chrome). @@ -231,7 +236,7 @@ UI.initConference = function () { UI.setUserAvatarID(id, Settings.getAvatarId()); } - Toolbar.checkAutoEnableDesktopSharing(); + APP.store.dispatch(checkAutoEnableDesktopSharing()); if(!interfaceConfig.filmStripOnly) { Feedback.init(eventEmitter); @@ -294,7 +299,6 @@ UI.start = function () { // Set the defaults for tooltips. _setTooltipDefaults(); - ToolbarToggler.init(); SideContainerToggler.init(eventEmitter); FilmStrip.init(eventEmitter); @@ -313,11 +317,9 @@ UI.start = function () { { leading: true, trailing: false }); $("#videoconference_page").mousemove(debouncedShowToolbar); - setupToolbars(); - // Initialise the recording module. - if (config.enableRecording) - Recording.init(eventEmitter, config.recordingType); + // Initialize side panels + SidePanels.init(eventEmitter); } else { $("body").addClass("filmstrip-only"); UIUtil.setVisible('mainToolbarContainer', false); @@ -446,7 +448,8 @@ UI.initEtherpad = name => { logger.log('Etherpad is enabled'); etherpadManager = new EtherpadManager(config.etherpad_base, name, eventEmitter); - Toolbar.showEtherpadButton(); + + APP.store.dispatch(showEtherpadButton()); }; /** @@ -521,8 +524,9 @@ UI.onPeerVideoTypeChanged UI.updateLocalRole = isModerator => { VideoLayout.showModeratorIndicator(); - Toolbar.showSipCallButton(isModerator); - Toolbar.showSharedVideoButton(isModerator); + APP.store.dispatch(showSIPCallButton(isModerator)); + APP.store.dispatch(showSharedVideoButton()); + Recording.showRecordingButton(isModerator); SettingsMenu.showStartMutedOptions(isModerator); SettingsMenu.showFollowMeOptions(isModerator); @@ -676,7 +680,10 @@ UI.askForNickname = function () { UI.setAudioMuted = function (id, muted) { VideoLayout.onAudioMute(id, muted); 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) { VideoLayout.onVideoMute(id, muted); 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 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) { let videos = $("#remoteVideos .videocontainer:not(#mixedstream)"); @@ -731,12 +741,12 @@ UI.clickOnVideo = function (videoNumber) { //Used by torture UI.showToolbar = function (timeout) { - return ToolbarToggler.showToolbar(timeout); + APP.store.dispatch(showToolbar(timeout)); }; //Used by torture 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. + * + * @returns {void} */ -UI.updateDesktopSharingButtons = function () { - Toolbar.updateDesktopSharingButtonState(); -}; +UI.updateDesktopSharingButtons + = () => + APP.store.dispatch(setToolbarButton('desktop', { + toggled: APP.conference.isSharingScreen + })); /** * Hide connection quality statistics from UI. @@ -970,11 +984,8 @@ UI.addMessage = function (from, displayName, message, stamp) { Chat.updateChatConversation(from, displayName, message, stamp); }; -// eslint-disable-next-line no-unused-vars -UI.updateDTMFSupport = function (isDTMFSupported) { - //TODO: enable when the UI is ready - //Toolbar.showDialPadButton(isDTMFSupported); -}; +UI.updateDTMFSupport + = isDTMFSupported => APP.store.dispatch(showDialPadButton(isDTMFSupported)); /** * 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 * or disabled */ -UI.setCameraButtonEnabled = enabled => Toolbar.setVideoIconEnabled(enabled); +UI.setCameraButtonEnabled + = enabled => APP.store.dispatch(setVideoIconEnabled(enabled)); /** * 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 * enabled or disabled */ -UI.setMicrophoneButtonEnabled = enabled => Toolbar.setAudioIconEnabled(enabled); +UI.setMicrophoneButtonEnabled + = enabled => APP.store.dispatch(setAudioIconEnabled(enabled)); UI.showRingOverlay = function () { RingOverlay.show(APP.tokenData.callee, interfaceConfig.DISABLE_RINGING); diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index 9cd9fd6dd..bc5b706da 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -20,7 +20,8 @@ import UIEvents from "../../../service/UI/UIEvents"; import UIUtil from '../util/UIUtil'; import VideoLayout from '../videolayout/VideoLayout'; import Feedback from '../feedback/Feedback.js'; -import Toolbar from '../toolbars/Toolbar'; + +import { hideToolbar } from '../../../react/features/toolbar'; /** * The dialog for user input. @@ -263,7 +264,7 @@ var Recording = { APP.conference.getMyUserId(), false); VideoLayout.setLocalVideoVisible(false); Feedback.enableFeedback(false); - Toolbar.enable(false); + APP.store.dispatch(hideToolbar()); APP.UI.messageHandler.enableNotifications(false); APP.UI.messageHandler.enablePopups(false); } diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index 11e218938..be52957b0 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -9,7 +9,8 @@ import VideoLayout from "../videolayout/VideoLayout"; import LargeContainer from '../videolayout/LargeContainer'; import SmallVideo from '../videolayout/SmallVideo'; import FilmStrip from '../videolayout/FilmStrip'; -import ToolbarToggler from "../toolbars/ToolbarToggler"; + +import { dockToolbar, showToolbar } from '../../../react/features/toolbar'; export const SHARED_VIDEO_CONTAINER_TYPE = "sharedvideo"; @@ -578,7 +579,7 @@ class SharedVideoContainer extends LargeContainer { self.bodyBackground = document.body.style.background; document.body.style.background = 'black'; this.$iframe.css({opacity: 1}); - ToolbarToggler.dockToolbar(true); + APP.store.dispatch(dockToolbar(true)); resolve(); }); }); @@ -586,7 +587,7 @@ class SharedVideoContainer extends LargeContainer { hide () { let self = this; - ToolbarToggler.dockToolbar(false); + APP.store.dispatch(dockToolbar(false)); return new Promise(resolve => { this.$iframe.fadeOut(300, () => { document.body.style.background = self.bodyBackground; @@ -597,7 +598,7 @@ class SharedVideoContainer extends LargeContainer { } onHoverIn () { - ToolbarToggler.showToolbar(); + APP.store.dispatch(showToolbar()); } get id () { diff --git a/modules/UI/side_pannels/chat/Chat.js b/modules/UI/side_pannels/chat/Chat.js index 8437838f0..872f8eb34 100644 --- a/modules/UI/side_pannels/chat/Chat.js +++ b/modules/UI/side_pannels/chat/Chat.js @@ -2,7 +2,6 @@ import {processReplacements, linkify} from './Replacement'; import CommandsProcessor from './Commands'; -import ToolbarToggler from '../../toolbars/ToolbarToggler'; import VideoLayout from "../../videolayout/VideoLayout"; import UIUtil from '../../util/UIUtil'; @@ -10,6 +9,8 @@ import UIEvents from '../../../../service/UI/UIEvents'; import { smileys } from './smileys'; +import { dockToolbar, setSubject } from '../../../../react/features/toolbar'; + let unreadMessages = 0; const sidePanelsContainerId = 'sideToolbarContainer'; const htmlStr = ` @@ -59,7 +60,7 @@ function updateVisualNotification() { if (unreadMessages) { unreadMsgElement.innerHTML = unreadMessages.toString(); - ToolbarToggler.dockToolbar(true); + APP.store.dispatch(dockToolbar(true)); const chatButtonElement = 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 // video mode. if (VideoLayout.isLargeVideoVisible()) { - ToolbarToggler.dockToolbar(false); + APP.store.dispatch(dockToolbar(false)); } // if we are in conversation mode focus on the text input @@ -319,10 +320,9 @@ var Chat = { subject = subject.trim(); } - let subjectId = 'subject'; const html = linkify(UIUtil.escapeHtml(subject)); - $(`#${subjectId}`).html(html); - UIUtil.setVisible(subjectId, subject && subject.length > 0); + + APP.store.dispatch(setSubject(html)); }, /** diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js deleted file mode 100644 index ca7adb1a0..000000000 --- a/modules/UI/toolbars/Toolbar.js +++ /dev/null @@ -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: ` - - ` - }, - 'contacts': { - id: 'toolbar_contact_list', - tooltipKey: 'bottomtoolbar.contactlist', - className: 'button icon-contactList', - sideContainerId: 'contacts_container', - html: ` - - ` - }, - 'profile': { - id: 'toolbar_button_profile', - tooltipKey: 'profile.setDisplayNameLabel', - className: 'button', - sideContainerId: 'profile_container', - html: `` - }, - '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 = (` - `); - - 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 string which - * identifies the toolbar button to be enabled/disabled. - * @param {boolean} enabled - true to enable the specified - * btn or false to disable it. - * @param {string} dataI18n - The value to assign to the data-i18n - * attribute of the specified btn. - * @param {string} shortcut - The value, if any, to assign to the - * shortcut attribute of the specified btn 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 true if currently visible, false - 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; diff --git a/modules/UI/toolbars/ToolbarToggler.js b/modules/UI/toolbars/ToolbarToggler.js deleted file mode 100644 index 05ff316fb..000000000 --- a/modules/UI/toolbars/ToolbarToggler.js +++ /dev/null @@ -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; diff --git a/react/features/conference/components/Conference.native.js b/react/features/conference/components/Conference.native.js index b473eda49..89ff89979 100644 --- a/react/features/conference/components/Conference.native.js +++ b/react/features/conference/components/Conference.native.js @@ -6,7 +6,7 @@ import { DialogContainer } from '../../base/dialog'; import { Container } from '../../base/react'; import { FilmStrip } from '../../film-strip'; import { LargeVideo } from '../../large-video'; -import { Toolbar } from '../../toolbar'; +import { setToolbarVisible, Toolbar } from '../../toolbar'; import { styles } from './styles'; @@ -28,8 +28,39 @@ class Conference extends Component { * @static */ 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. @@ -40,8 +71,6 @@ class Conference extends Component { constructor(props) { super(props); - this.state = { toolbarVisible: true }; - /** * The numerical ID of the timeout in milliseconds after which the * toolbar will be hidden. To be used with @@ -62,7 +91,7 @@ class Conference extends Component { * returns {void} */ componentDidMount() { - this._setToolbarTimeout(this.state.toolbarVisible); + this._setToolbarTimeout(this.props._toolbarVisible); } /** @@ -72,7 +101,7 @@ class Conference extends Component { * @returns {void} */ componentWillMount() { - this.props.dispatch(connect()); + this.props._onConnect(); } /** @@ -85,7 +114,7 @@ class Conference extends Component { componentWillUnmount() { this._clearToolbarTimeout(); - this.props.dispatch(disconnect()); + this.props._onDisconnect(); } /** @@ -95,8 +124,6 @@ class Conference extends Component { * @returns {ReactElement} */ render() { - const toolbarVisible = this.state.toolbarVisible; - return ( - - + + + - ); } @@ -135,10 +162,9 @@ class Conference extends Component { * @returns {void} */ _onClick() { - const toolbarVisible = !this.state.toolbarVisible; - - this.setState({ toolbarVisible }); + const toolbarVisible = !this.props._toolbarVisible; + this.props._setToolbarVisible(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); diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js index 90bd2b8ef..35150a2a5 100644 --- a/react/features/conference/components/Conference.web.js +++ b/react/features/conference/components/Conference.web.js @@ -6,9 +6,8 @@ import { connect as reactReduxConnect } from 'react-redux'; import { connect, disconnect } from '../../base/connection'; import { DialogContainer } from '../../base/dialog'; import { Watermarks } from '../../base/react'; -import { FeedbackButton } from '../../feedback'; import { OverlayContainer } from '../../overlay'; -import { Notice } from '../../toolbar'; +import { Toolbar } from '../../toolbar'; import { HideNotificationBarStyle } from '../../unsupported-browser'; declare var $: Function; @@ -66,25 +65,8 @@ class Conference extends Component { render() { return (
-
- + -
-
-
-
-
- - - -
-
+ diff --git a/react/features/film-strip/components/FilmStrip.js b/react/features/film-strip/components/FilmStrip.js index 399f82c82..d822d5672 100644 --- a/react/features/film-strip/components/FilmStrip.js +++ b/react/features/film-strip/components/FilmStrip.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; -import { connect } from 'react-redux'; import { ScrollView } from 'react-native'; +import { connect } from 'react-redux'; import { Container } from '../../base/react'; @@ -33,7 +33,7 @@ class FilmStrip extends Component { * @private * @type {boolean} */ - visible: React.PropTypes.bool.isRequired + _visible: React.PropTypes.bool.isRequired } /** @@ -45,7 +45,7 @@ class FilmStrip extends Component { return ( + visible = { this.props._visible }> , 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 + })); + }; +} diff --git a/react/features/toolbar/actions.web.js b/react/features/toolbar/actions.web.js new file mode 100644 index 000000000..a137c74eb --- /dev/null +++ b/react/features/toolbar/actions.web.js @@ -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; + } + } + }; +} diff --git a/react/features/toolbar/components/AbstractToolbar.js b/react/features/toolbar/components/AbstractToolbar.js deleted file mode 100644 index 59f9dcdd0..000000000 --- a/react/features/toolbar/components/AbstractToolbar.js +++ /dev/null @@ -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 - }; -} diff --git a/react/features/toolbar/components/BaseToolbar.web.js b/react/features/toolbar/components/BaseToolbar.web.js new file mode 100644 index 000000000..ebb356738 --- /dev/null +++ b/react/features/toolbar/components/BaseToolbar.web.js @@ -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 ( +
+ { + [ ...this.props.toolbarButtons.entries() ] + .reduce(this._renderToolbarButton, []) + } + { + this.props.children + } +
+ ); + } + + /** + * 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> { + const [ key, button ] = keyValuePair; + const { splitterIndex } = this.props; + + if (splitterIndex && index === splitterIndex) { + const splitter = ; + + acc.push(splitter); + } + + const { onClick, onMount, onUnmount } = button; + + acc.push( + + ); + + 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); diff --git a/react/features/toolbar/components/PrimaryToolbar.web.js b/react/features/toolbar/components/PrimaryToolbar.web.js new file mode 100644 index 000000000..2058c334a --- /dev/null +++ b/react/features/toolbar/components/PrimaryToolbar.web.js @@ -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 ( + + ); + } +} + +/** + * 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); + diff --git a/react/features/toolbar/components/SecondaryToolbar.web.js b/react/features/toolbar/components/SecondaryToolbar.web.js new file mode 100644 index 000000000..36d28cfac --- /dev/null +++ b/react/features/toolbar/components/SecondaryToolbar.web.js @@ -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 ( + + + + ); + } +} + +/** + * 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); diff --git a/react/features/toolbar/components/Toolbar.native.js b/react/features/toolbar/components/Toolbar.native.js index 9bc563054..0cb72a357 100644 --- a/react/features/toolbar/components/Toolbar.native.js +++ b/react/features/toolbar/components/Toolbar.native.js @@ -1,41 +1,74 @@ -import React from 'react'; +import React, { Component } from 'react'; import { View } from 'react-native'; import { connect } from 'react-redux'; import { MEDIA_TYPE, toggleCameraFacingMode } from '../../base/media'; import { Container } from '../../base/react'; 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 ToolbarButton from './ToolbarButton'; /** * Implements the conference toolbar on React Native. - * - * @extends AbstractToolbar */ -class Toolbar extends AbstractToolbar { +class Toolbar extends Component { /** * Toolbar component's property types. * * @static */ - static propTypes = AbstractToolbar.propTypes + static propTypes = { + /** + * Flag showing that audio is muted. + */ + _audioMuted: React.PropTypes.bool, - /** - * Initializes a new Toolbar instance. - * - * @param {Object} props - The read-only React Component props with which - * the new instance is to be initialized. - */ - constructor(props) { - super(props); + /** + * Flag showing whether room is locked. + */ + _locked: React.PropTypes.bool, - // Bind event handlers so they are only bound once for every instance. - this._toggleCameraFacingMode - = this._toggleCameraFacingMode.bind(this); - } + /** + * Handler for hangup. + */ + _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()}. @@ -47,7 +80,7 @@ class Toolbar extends AbstractToolbar { return ( + visible = { this.props._visible }> { 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, * audio and video mute. @@ -76,12 +146,12 @@ class Toolbar extends AbstractToolbar { ); @@ -125,7 +195,7 @@ class Toolbar extends AbstractToolbar { */} @@ -134,7 +204,7 @@ class Toolbar extends AbstractToolbar { this.props._locked ? 'security-locked' : 'security' } iconStyle = { iconStyle } - onClick = { this._onRoomLock } + onClick = { this.props._onRoomLock } style = { style } underlayColor = { underlayColor } /> @@ -142,17 +212,6 @@ class Toolbar extends AbstractToolbar { /* 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' }); -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); diff --git a/react/features/toolbar/components/Toolbar.web.js b/react/features/toolbar/components/Toolbar.web.js index e69de29bb..12865357a 100644 --- a/react/features/toolbar/components/Toolbar.web.js +++ b/react/features/toolbar/components/Toolbar.web.js @@ -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 ( +
+ { + this._renderSubject() + } + { + this._renderToolbars() + } +
+
+ ); + } + + /** + * 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 ( +
+ ); + } + + /** + * 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 ( +
+ + + +
+ ); + } +} + +/** + * 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); diff --git a/react/features/toolbar/components/ToolbarButton.web.js b/react/features/toolbar/components/ToolbarButton.web.js new file mode 100644 index 000000000..a0773519b --- /dev/null +++ b/react/features/toolbar/components/ToolbarButton.web.js @@ -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 ( + + { this._renderInnerElementsIfRequired() } + { this._renderPopups(popups) } + + ); + } + + /** + * 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 ( +
+ ); + }); + } + + /** + * 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); diff --git a/react/features/toolbar/components/index.js b/react/features/toolbar/components/index.js index 4acd03810..dae206cee 100644 --- a/react/features/toolbar/components/index.js +++ b/react/features/toolbar/components/index.js @@ -1,2 +1 @@ -export { default as Notice } from './Notice'; export { default as Toolbar } from './Toolbar'; diff --git a/react/features/toolbar/defaultToolbarButtons.js b/react/features/toolbar/defaultToolbarButtons.js new file mode 100644 index 000000000..161099f0b --- /dev/null +++ b/react/features/toolbar/defaultToolbarButtons.js @@ -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 + = ``; + + 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: + + , + 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: + 1 + , + 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: , + 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' + } +}; diff --git a/react/features/toolbar/functions.js b/react/features/toolbar/functions.js new file mode 100644 index 000000000..f39646284 --- /dev/null +++ b/react/features/toolbar/functions.js @@ -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'); + } +} diff --git a/react/features/toolbar/index.js b/react/features/toolbar/index.js index 07635cbbc..a29aa08e0 100644 --- a/react/features/toolbar/index.js +++ b/react/features/toolbar/index.js @@ -1 +1,6 @@ +export * from './actions'; +export * from './actionTypes'; export * from './components'; + +import './middleware'; +import './reducer'; diff --git a/react/features/toolbar/middleware.js b/react/features/toolbar/middleware.js new file mode 100644 index 000000000..e60b86a11 --- /dev/null +++ b/react/features/toolbar/middleware.js @@ -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); +}); diff --git a/react/features/toolbar/reducer.js b/react/features/toolbar/reducer.js new file mode 100644 index 000000000..bec9b037b --- /dev/null +++ b/react/features/toolbar/reducer.js @@ -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) + }; +}