diff --git a/conference.js b/conference.js index 4eb0f7002..7c4c0084b 100644 --- a/conference.js +++ b/conference.js @@ -38,6 +38,7 @@ import { conferenceFailed, conferenceJoined, conferenceLeft, + conferenceSubjectChanged, conferenceWillJoin, conferenceWillLeave, dataChannelOpened, @@ -45,8 +46,7 @@ import { onStartMutedPolicyChanged, p2pStatusChanged, sendLocalParticipant, - setDesktopSharingEnabled, - setSubject + setDesktopSharingEnabled } from './react/features/base/conference'; import { getAvailableDevices, @@ -1834,7 +1834,7 @@ export default { APP.UI.showToolbar(6000); }); room.on(JitsiConferenceEvents.SUBJECT_CHANGED, - subject => APP.API.notifySubjectChanged(subject)); + subject => APP.store.dispatch(conferenceSubjectChanged(subject))); room.on( JitsiConferenceEvents.LAST_N_ENDPOINTS_CHANGED, @@ -2767,16 +2767,6 @@ export default { APP.API.notifyAudioMutedStatusChanged(muted); }, - /** - * Changes the subject of the conference. - * Note: available only for moderator. - * - * @param subject {string} the new subject for the conference. - */ - setSubject(subject) { - APP.store.dispatch(setSubject(subject)); - }, - /** * Dispatches the passed in feedback for submission. The submitted score * should be a number inclusively between 1 through 5, or -1 for no score. diff --git a/css/_subject.scss b/css/_subject.scss new file mode 100644 index 000000000..9cf84c9c3 --- /dev/null +++ b/css/_subject.scss @@ -0,0 +1,21 @@ +.subject { + top: -120px; + transition: top .3s ease-in; + height: 95px; + width: 100%; + position: absolute; + padding: 25px 140px 0 140px; + text-align: center; + font-size: 17px; + color: #fff; + z-index: $toolbarBackgroundZ; + overflow: hidden; + text-overflow: ellipsis; + box-sizing: border-box; + white-space: nowrap; + background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)); + + &.visible { + top: 0px; + } +} diff --git a/css/main.scss b/css/main.scss index 6de4adb98..d4404b87d 100644 --- a/css/main.scss +++ b/css/main.scss @@ -49,6 +49,7 @@ $flagsImagePath: "/images/"; @import 'modals/local-recording/local-recording'; @import 'videolayout_default'; @import 'notice'; +@import 'subject'; @import 'popup_menu'; @import 'recording'; @import 'login_menu'; diff --git a/modules/API/API.js b/modules/API/API.js index 892797a85..4463ea357 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -5,6 +5,7 @@ import { createApiEvent, sendAnalytics } from '../../react/features/analytics'; +import { setSubject } from '../../react/features/base/conference'; import { parseJWTFromURLParams } from '../../react/features/base/jwt'; import { invite } from '../../react/features/invite'; import { getJitsiMeetTransport } from '../transport'; @@ -65,7 +66,7 @@ function initCommands() { }, 'subject': subject => { sendAnalytics(createApiEvent('subject.changed')); - APP.conference.setSubject(subject); + APP.store.dispatch(setSubject(subject)); }, 'submit-feedback': feedback => { sendAnalytics(createApiEvent('submit.feedback')); diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js index 40b83006b..fead06129 100644 --- a/modules/API/external/external_api.js +++ b/modules/API/external/external_api.js @@ -548,7 +548,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter { * {@code displayName} - Sets the display name of the local participant to * the value passed in the arguments array. * {@code subject} - Sets the subject of the conference, the value passed - * in the arguments array. Note: available only for moderator. + * in the arguments array. Note: Available only for moderator. * * {@code toggleAudio} - Mutes / unmutes audio with no arguments. * {@code toggleVideo} - Mutes / unmutes video with no arguments. diff --git a/react/features/base/conference/actionTypes.js b/react/features/base/conference/actionTypes.js index e28c01dd9..43ed53a39 100644 --- a/react/features/base/conference/actionTypes.js +++ b/react/features/base/conference/actionTypes.js @@ -42,6 +42,16 @@ export const CONFERENCE_JOINED = Symbol('CONFERENCE_JOINED'); */ export const CONFERENCE_LEFT = Symbol('CONFERENCE_LEFT'); +/** + * The type of (redux) action, which indicates conference subject changes. + * + * { + * type: CONFERENCE_SUBJECT_CHANGED + * subject: string + * } + */ +export const CONFERENCE_SUBJECT_CHANGED = Symbol('CONFERENCE_SUBJECT_CHANGED'); + /** * The type of (redux) action which signals that a specific conference will be * joined. @@ -119,16 +129,6 @@ export const P2P_STATUS_CHANGED = Symbol('P2P_STATUS_CHANGED'); */ export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY'); -/** - * The type of (redux) action, which indicates to set conference subject. - * - * { - * type: SET_CONFERENCE_SUBJECT - * subject: string - * } - */ -export const SET_CONFERENCE_SUBJECT = Symbol('SET_CONFERENCE_SUBJECT'); - /** * The type of (redux) action which sets the desktop sharing enabled flag for * the current conference. @@ -199,6 +199,16 @@ export const SET_PASSWORD = Symbol('SET_PASSWORD'); */ export const SET_PASSWORD_FAILED = Symbol('SET_PASSWORD_FAILED'); +/** + * The type of (redux) action which signals for pending subject changes. + * + * { + * type: SET_PENDING_SUBJECT_CHANGE, + * subject: string + * } + */ +export const SET_PENDING_SUBJECT_CHANGE = Symbol('SET_PENDING_SUBJECT_CHANGE'); + /** * The type of (redux) action which sets the preferred maximum video height that * should be received from remote participants. diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js index 31158b585..aa669aec5 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -26,6 +26,7 @@ import { CONFERENCE_FAILED, CONFERENCE_JOINED, CONFERENCE_LEFT, + CONFERENCE_SUBJECT_CHANGED, CONFERENCE_WILL_JOIN, CONFERENCE_WILL_LEAVE, DATA_CHANNEL_OPENED, @@ -33,7 +34,6 @@ import { LOCK_STATE_CHANGED, P2P_STATUS_CHANGED, SET_AUDIO_ONLY, - SET_CONFERENCE_SUBJECT, SET_DESKTOP_SHARING_ENABLED, SET_FOLLOW_ME, SET_LASTN, @@ -42,6 +42,7 @@ import { SET_PASSWORD_FAILED, SET_PREFERRED_RECEIVER_VIDEO_QUALITY, SET_ROOM, + SET_PENDING_SUBJECT_CHANGE, SET_START_MUTED_POLICY } from './actionTypes'; import { @@ -272,6 +273,22 @@ export function conferenceLeft(conference: Object) { }; } +/** + * Signals that the conference subject has been changed. + * + * @param {string} subject - The new subject. + * @returns {{ + * type: CONFERENCE_SUBJECT_CHANGED, + * subject: string + * }} + */ +export function conferenceSubjectChanged(subject: string) { + return { + type: CONFERENCE_SUBJECT_CHANGED, + subject + }; +} + /** * Adds any existing local tracks to a specific conference before the conference * is joined. Then signals the intention of the application to have the local @@ -736,9 +753,21 @@ export function toggleAudioOnly() { * @param {string} subject - The new subject. * @returns {void} */ -export function setSubject(subject: String) { - return { - type: SET_CONFERENCE_SUBJECT, - subject +export function setSubject(subject: string = '') { + return (dispatch: Dispatch<*>, getState: Function) => { + const { conference } = getState()['features/base/conference']; + + if (conference) { + dispatch({ + type: SET_PENDING_SUBJECT_CHANGE, + subject: undefined + }); + conference.setSubject(subject); + } else { + dispatch({ + type: SET_PENDING_SUBJECT_CHANGE, + subject + }); + } }; } diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.js index 134df915a..a2d1c74eb 100644 --- a/react/features/base/conference/middleware.js +++ b/react/features/base/conference/middleware.js @@ -27,15 +27,16 @@ import { conferenceLeft, conferenceWillLeave, createConference, - setLastN + setLastN, + setSubject } from './actions'; import { CONFERENCE_FAILED, CONFERENCE_JOINED, + CONFERENCE_SUBJECT_CHANGED, CONFERENCE_WILL_LEAVE, DATA_CHANNEL_OPENED, SET_AUDIO_ONLY, - SET_CONFERENCE_SUBJECT, SET_LASTN, SET_ROOM } from './actionTypes'; @@ -75,6 +76,9 @@ MiddlewareRegistry.register(store => next => action => { case CONNECTION_FAILED: return _connectionFailed(store, next, action); + case CONFERENCE_SUBJECT_CHANGED: + return _conferenceSubjectChanged(store, next, action); + case CONFERENCE_WILL_LEAVE: _conferenceWillLeave(); break; @@ -91,9 +95,6 @@ MiddlewareRegistry.register(store => next => action => { case SET_AUDIO_ONLY: return _setAudioOnly(store, next, action); - case SET_CONFERENCE_SUBJECT: - return _setSubject(store, next, action); - case SET_LASTN: return _setLastN(store, next, action); @@ -192,7 +193,15 @@ function _conferenceFailed(store, next, action) { function _conferenceJoined({ dispatch, getState }, next, action) { const result = next(action); - const { audioOnly, conference } = getState()['features/base/conference']; + const { + audioOnly, + conference, + pendingSubjectChange + } = getState()['features/base/conference']; + + if (pendingSubjectChange) { + dispatch(setSubject(pendingSubjectChange)); + } // FIXME On Web the audio only mode for "start audio only" is toggled before // conference is added to the redux store ("on conference joined" action) @@ -305,6 +314,29 @@ function _connectionFailed({ dispatch, getState }, next, action) { return result; } +/** + * Notifies the feature base/conference that the action + * {@code CONFERENCE_SUBJECT_CHANGED} is being dispatched within a specific + * redux store. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The redux action {@code CONFERENCE_SUBJECT_CHANGED} + * which is being dispatched in the specified {@code store}. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _conferenceSubjectChanged({ getState }, next, action) { + const result = next(action); + const { subject } = getState()['features/base/conference']; + + typeof APP === 'object' && APP.API.notifySubjectChanged(subject); + + return result; +} + /** * Notifies the feature base/conference that the action * {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux @@ -683,26 +715,3 @@ function _updateLocalParticipantInConference({ getState }, next, action) { return result; } - -/** - * Changing conference subject. - * - * @param {Store} store - The redux store in which the specified {@code action} - * is being dispatched. - * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the - * specified {@code action} to the specified {@code store}. - * @param {Action} action - The redux action which is being dispatched in the - * specified {@code store}. - * @private - * @returns {Object} The value returned by {@code next(action)}. - */ -function _setSubject({ getState }, next, action) { - const { conference } = getState()['features/base/conference']; - const { subject } = action; - - if (subject) { - conference.setSubject(subject); - } - - return next(action); -} diff --git a/react/features/base/conference/reducer.js b/react/features/base/conference/reducer.js index 43ac2df34..7a4a361db 100644 --- a/react/features/base/conference/reducer.js +++ b/react/features/base/conference/reducer.js @@ -10,6 +10,7 @@ import { CONFERENCE_FAILED, CONFERENCE_JOINED, CONFERENCE_LEFT, + CONFERENCE_SUBJECT_CHANGED, CONFERENCE_WILL_JOIN, CONFERENCE_WILL_LEAVE, LOCK_STATE_CHANGED, @@ -19,6 +20,7 @@ import { SET_FOLLOW_ME, SET_MAX_RECEIVER_VIDEO_QUALITY, SET_PASSWORD, + SET_PENDING_SUBJECT_CHANGE, SET_PREFERRED_RECEIVER_VIDEO_QUALITY, SET_ROOM, SET_SIP_GATEWAY_ENABLED, @@ -55,6 +57,9 @@ ReducerRegistry.register( case CONFERENCE_JOINED: return _conferenceJoined(state, action); + case CONFERENCE_SUBJECT_CHANGED: + return set(state, 'subject', action.subject); + case CONFERENCE_LEFT: case CONFERENCE_WILL_LEAVE: return _conferenceLeftOrWillLeave(state, action); @@ -92,6 +97,9 @@ ReducerRegistry.register( case SET_PASSWORD: return _setPassword(state, action); + case SET_PENDING_SUBJECT_CHANGE: + return set(state, 'pendingSubjectChange', action.subject); + case SET_PREFERRED_RECEIVER_VIDEO_QUALITY: return set( state, diff --git a/react/features/conference/components/web/Conference.js b/react/features/conference/components/web/Conference.js index 82a5c146d..c938f1daf 100644 --- a/react/features/conference/components/web/Conference.js +++ b/react/features/conference/components/web/Conference.js @@ -31,6 +31,7 @@ import { maybeShowSuboptimalExperienceNotification } from '../../functions'; import Labels from './Labels'; import { default as Notice } from './Notice'; +import { default as Subject } from './Subject'; declare var APP: Object; declare var config: Object; @@ -217,6 +218,7 @@ class Conference extends Component { id = 'videoconference_page' onMouseMove = { this._onShowToolbar }> +
{ hideVideoQualityLabel diff --git a/react/features/conference/components/web/Subject.js b/react/features/conference/components/web/Subject.js new file mode 100644 index 000000000..1239a2d8d --- /dev/null +++ b/react/features/conference/components/web/Subject.js @@ -0,0 +1,67 @@ +/* @flow */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { isToolboxVisible } from '../../../toolbox'; + +/** + * The type of the React {@code Component} props of {@link Subject}. + */ +type Props = { + + /** + * The subject of the conference. + */ + _subject: string, + + /** + * Indicates whether the component should be visible or not. + */ + _visible: boolean +}; + +/** + * Subject react component. + * + * @class Subject + */ +class Subject extends Component { + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { _subject, _visible } = this.props; + + return ( +
+ { _subject } +
+ ); + } +} + +/** + * Maps (parts of) the Redux state to the associated + * {@code Subject}'s props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _subject: string, + * _visible: boolean + * }} + */ +function _mapStateToProps(state) { + const { subject } = state['features/base/conference']; + + return { + _subject: subject, + _visible: isToolboxVisible(state) + }; +} +export default connect(_mapStateToProps)(Subject); diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js index 31c418f00..2930a7466 100644 --- a/react/features/toolbox/components/web/Toolbox.js +++ b/react/features/toolbox/components/web/Toolbox.js @@ -55,6 +55,7 @@ import { setToolbarHovered } from '../../actions'; import AudioMuteButton from '../AudioMuteButton'; +import { isToolboxVisible } from '../../functions'; import HangupButton from '../HangupButton'; import OverflowMenuButton from './OverflowMenuButton'; import OverflowMenuProfileItem from './OverflowMenuProfileItem'; @@ -1281,11 +1282,8 @@ function _mapStateToProps(state) { } = state['features/base/config']; const sharedVideoStatus = state['features/shared-video'].status; const { - alwaysVisible, fullScreen, - overflowMenuVisible, - timeoutID, - visible + overflowMenuVisible } = state['features/toolbox']; const localParticipant = getLocalParticipant(state); const localRecordingStates = state['features/local-recording']; @@ -1333,7 +1331,7 @@ function _mapStateToProps(state) { _sharingVideo: sharedVideoStatus === 'playing' || sharedVideoStatus === 'start' || sharedVideoStatus === 'pause', - _visible: Boolean(timeoutID || visible || alwaysVisible), + _visible: isToolboxVisible(state), // XXX: We are not currently using state here, but in the future, when // interfaceConfig is part of redux we will. diff --git a/react/features/toolbox/functions.any.js b/react/features/toolbox/functions.any.js deleted file mode 100644 index 4b24a01cc..000000000 --- a/react/features/toolbox/functions.any.js +++ /dev/null @@ -1,17 +0,0 @@ -// @flow - -import { toState } from '../base/redux'; - -/** - * Returns true if the toolbox is visible. - * - * @param {Object | Function} stateful - A function or object that can be - * resolved to Redux state by the function {@code toState}. - * @returns {boolean} - */ -export function isToolboxVisible(stateful: Object | Function) { - const { alwaysVisible, enabled, visible } - = toState(stateful)['features/toolbox']; - - return enabled && (alwaysVisible || visible); -} diff --git a/react/features/toolbox/functions.native.js b/react/features/toolbox/functions.native.js index 44e9d39cb..4b24a01cc 100644 --- a/react/features/toolbox/functions.native.js +++ b/react/features/toolbox/functions.native.js @@ -1,3 +1,17 @@ // @flow -export * from './functions.any'; +import { toState } from '../base/redux'; + +/** + * Returns true if the toolbox is visible. + * + * @param {Object | Function} stateful - A function or object that can be + * resolved to Redux state by the function {@code toState}. + * @returns {boolean} + */ +export function isToolboxVisible(stateful: Object | Function) { + const { alwaysVisible, enabled, visible } + = toState(stateful)['features/toolbox']; + + return enabled && (alwaysVisible || visible); +} diff --git a/react/features/toolbox/functions.web.js b/react/features/toolbox/functions.web.js index c196eb61b..be1cd9ef3 100644 --- a/react/features/toolbox/functions.web.js +++ b/react/features/toolbox/functions.web.js @@ -1,7 +1,5 @@ // @flow -export * from './functions.any'; - declare var interfaceConfig: Object; /** @@ -26,3 +24,21 @@ export function getToolboxHeight() { export function isButtonEnabled(name: string) { return interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1; } + + +/** + * Indicates if the toolbox is visible or not. + * + * @param {string} state - The state from the Redux store. + * @returns {boolean} - True to indicate that the toolbox is visible, false - + * otherwise. + */ +export function isToolboxVisible(state: Object) { + const { + alwaysVisible, + timeoutID, + visible + } = state['features/toolbox']; + + return Boolean(timeoutID || visible || alwaysVisible); +}