feat(subject): UI

This commit is contained in:
Hristo Terezov 2019-03-12 17:45:53 +00:00
parent 2715e81f1d
commit cb8e9eed5e
15 changed files with 233 additions and 84 deletions

View File

@ -38,6 +38,7 @@ import {
conferenceFailed, conferenceFailed,
conferenceJoined, conferenceJoined,
conferenceLeft, conferenceLeft,
conferenceSubjectChanged,
conferenceWillJoin, conferenceWillJoin,
conferenceWillLeave, conferenceWillLeave,
dataChannelOpened, dataChannelOpened,
@ -45,8 +46,7 @@ import {
onStartMutedPolicyChanged, onStartMutedPolicyChanged,
p2pStatusChanged, p2pStatusChanged,
sendLocalParticipant, sendLocalParticipant,
setDesktopSharingEnabled, setDesktopSharingEnabled
setSubject
} from './react/features/base/conference'; } from './react/features/base/conference';
import { import {
getAvailableDevices, getAvailableDevices,
@ -1834,7 +1834,7 @@ export default {
APP.UI.showToolbar(6000); APP.UI.showToolbar(6000);
}); });
room.on(JitsiConferenceEvents.SUBJECT_CHANGED, room.on(JitsiConferenceEvents.SUBJECT_CHANGED,
subject => APP.API.notifySubjectChanged(subject)); subject => APP.store.dispatch(conferenceSubjectChanged(subject)));
room.on( room.on(
JitsiConferenceEvents.LAST_N_ENDPOINTS_CHANGED, JitsiConferenceEvents.LAST_N_ENDPOINTS_CHANGED,
@ -2767,16 +2767,6 @@ export default {
APP.API.notifyAudioMutedStatusChanged(muted); 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 * Dispatches the passed in feedback for submission. The submitted score
* should be a number inclusively between 1 through 5, or -1 for no score. * should be a number inclusively between 1 through 5, or -1 for no score.

21
css/_subject.scss Normal file
View File

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

View File

@ -49,6 +49,7 @@ $flagsImagePath: "/images/";
@import 'modals/local-recording/local-recording'; @import 'modals/local-recording/local-recording';
@import 'videolayout_default'; @import 'videolayout_default';
@import 'notice'; @import 'notice';
@import 'subject';
@import 'popup_menu'; @import 'popup_menu';
@import 'recording'; @import 'recording';
@import 'login_menu'; @import 'login_menu';

View File

@ -5,6 +5,7 @@ import {
createApiEvent, createApiEvent,
sendAnalytics sendAnalytics
} from '../../react/features/analytics'; } from '../../react/features/analytics';
import { setSubject } from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt'; import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import { invite } from '../../react/features/invite'; import { invite } from '../../react/features/invite';
import { getJitsiMeetTransport } from '../transport'; import { getJitsiMeetTransport } from '../transport';
@ -65,7 +66,7 @@ function initCommands() {
}, },
'subject': subject => { 'subject': subject => {
sendAnalytics(createApiEvent('subject.changed')); sendAnalytics(createApiEvent('subject.changed'));
APP.conference.setSubject(subject); APP.store.dispatch(setSubject(subject));
}, },
'submit-feedback': feedback => { 'submit-feedback': feedback => {
sendAnalytics(createApiEvent('submit.feedback')); sendAnalytics(createApiEvent('submit.feedback'));

View File

@ -548,7 +548,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* {@code displayName} - Sets the display name of the local participant to * {@code displayName} - Sets the display name of the local participant to
* the value passed in the arguments array. * the value passed in the arguments array.
* {@code subject} - Sets the subject of the conference, the value passed * {@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 toggleAudio} - Mutes / unmutes audio with no arguments.
* {@code toggleVideo} - Mutes / unmutes video with no arguments. * {@code toggleVideo} - Mutes / unmutes video with no arguments.

View File

@ -42,6 +42,16 @@ export const CONFERENCE_JOINED = Symbol('CONFERENCE_JOINED');
*/ */
export const CONFERENCE_LEFT = Symbol('CONFERENCE_LEFT'); 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 * The type of (redux) action which signals that a specific conference will be
* joined. * joined.
@ -119,16 +129,6 @@ export const P2P_STATUS_CHANGED = Symbol('P2P_STATUS_CHANGED');
*/ */
export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY'); 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 type of (redux) action which sets the desktop sharing enabled flag for
* the current conference. * the current conference.
@ -199,6 +199,16 @@ export const SET_PASSWORD = Symbol('SET_PASSWORD');
*/ */
export const SET_PASSWORD_FAILED = Symbol('SET_PASSWORD_FAILED'); 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 * The type of (redux) action which sets the preferred maximum video height that
* should be received from remote participants. * should be received from remote participants.

View File

@ -26,6 +26,7 @@ import {
CONFERENCE_FAILED, CONFERENCE_FAILED,
CONFERENCE_JOINED, CONFERENCE_JOINED,
CONFERENCE_LEFT, CONFERENCE_LEFT,
CONFERENCE_SUBJECT_CHANGED,
CONFERENCE_WILL_JOIN, CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE, CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED, DATA_CHANNEL_OPENED,
@ -33,7 +34,6 @@ import {
LOCK_STATE_CHANGED, LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED, P2P_STATUS_CHANGED,
SET_AUDIO_ONLY, SET_AUDIO_ONLY,
SET_CONFERENCE_SUBJECT,
SET_DESKTOP_SHARING_ENABLED, SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME, SET_FOLLOW_ME,
SET_LASTN, SET_LASTN,
@ -42,6 +42,7 @@ import {
SET_PASSWORD_FAILED, SET_PASSWORD_FAILED,
SET_PREFERRED_RECEIVER_VIDEO_QUALITY, SET_PREFERRED_RECEIVER_VIDEO_QUALITY,
SET_ROOM, SET_ROOM,
SET_PENDING_SUBJECT_CHANGE,
SET_START_MUTED_POLICY SET_START_MUTED_POLICY
} from './actionTypes'; } from './actionTypes';
import { 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 * 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 * 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. * @param {string} subject - The new subject.
* @returns {void} * @returns {void}
*/ */
export function setSubject(subject: String) { export function setSubject(subject: string = '') {
return { return (dispatch: Dispatch<*>, getState: Function) => {
type: SET_CONFERENCE_SUBJECT, const { conference } = getState()['features/base/conference'];
subject
if (conference) {
dispatch({
type: SET_PENDING_SUBJECT_CHANGE,
subject: undefined
});
conference.setSubject(subject);
} else {
dispatch({
type: SET_PENDING_SUBJECT_CHANGE,
subject
});
}
}; };
} }

View File

@ -27,15 +27,16 @@ import {
conferenceLeft, conferenceLeft,
conferenceWillLeave, conferenceWillLeave,
createConference, createConference,
setLastN setLastN,
setSubject
} from './actions'; } from './actions';
import { import {
CONFERENCE_FAILED, CONFERENCE_FAILED,
CONFERENCE_JOINED, CONFERENCE_JOINED,
CONFERENCE_SUBJECT_CHANGED,
CONFERENCE_WILL_LEAVE, CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED, DATA_CHANNEL_OPENED,
SET_AUDIO_ONLY, SET_AUDIO_ONLY,
SET_CONFERENCE_SUBJECT,
SET_LASTN, SET_LASTN,
SET_ROOM SET_ROOM
} from './actionTypes'; } from './actionTypes';
@ -75,6 +76,9 @@ MiddlewareRegistry.register(store => next => action => {
case CONNECTION_FAILED: case CONNECTION_FAILED:
return _connectionFailed(store, next, action); return _connectionFailed(store, next, action);
case CONFERENCE_SUBJECT_CHANGED:
return _conferenceSubjectChanged(store, next, action);
case CONFERENCE_WILL_LEAVE: case CONFERENCE_WILL_LEAVE:
_conferenceWillLeave(); _conferenceWillLeave();
break; break;
@ -91,9 +95,6 @@ MiddlewareRegistry.register(store => next => action => {
case SET_AUDIO_ONLY: case SET_AUDIO_ONLY:
return _setAudioOnly(store, next, action); return _setAudioOnly(store, next, action);
case SET_CONFERENCE_SUBJECT:
return _setSubject(store, next, action);
case SET_LASTN: case SET_LASTN:
return _setLastN(store, next, action); return _setLastN(store, next, action);
@ -192,7 +193,15 @@ function _conferenceFailed(store, next, action) {
function _conferenceJoined({ dispatch, getState }, next, action) { function _conferenceJoined({ dispatch, getState }, next, action) {
const result = 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 // 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) // conference is added to the redux store ("on conference joined" action)
@ -305,6 +314,29 @@ function _connectionFailed({ dispatch, getState }, next, action) {
return result; 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 * Notifies the feature base/conference that the action
* {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux * {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux
@ -683,26 +715,3 @@ function _updateLocalParticipantInConference({ getState }, next, action) {
return result; 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);
}

View File

@ -10,6 +10,7 @@ import {
CONFERENCE_FAILED, CONFERENCE_FAILED,
CONFERENCE_JOINED, CONFERENCE_JOINED,
CONFERENCE_LEFT, CONFERENCE_LEFT,
CONFERENCE_SUBJECT_CHANGED,
CONFERENCE_WILL_JOIN, CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE, CONFERENCE_WILL_LEAVE,
LOCK_STATE_CHANGED, LOCK_STATE_CHANGED,
@ -19,6 +20,7 @@ import {
SET_FOLLOW_ME, SET_FOLLOW_ME,
SET_MAX_RECEIVER_VIDEO_QUALITY, SET_MAX_RECEIVER_VIDEO_QUALITY,
SET_PASSWORD, SET_PASSWORD,
SET_PENDING_SUBJECT_CHANGE,
SET_PREFERRED_RECEIVER_VIDEO_QUALITY, SET_PREFERRED_RECEIVER_VIDEO_QUALITY,
SET_ROOM, SET_ROOM,
SET_SIP_GATEWAY_ENABLED, SET_SIP_GATEWAY_ENABLED,
@ -55,6 +57,9 @@ ReducerRegistry.register(
case CONFERENCE_JOINED: case CONFERENCE_JOINED:
return _conferenceJoined(state, action); return _conferenceJoined(state, action);
case CONFERENCE_SUBJECT_CHANGED:
return set(state, 'subject', action.subject);
case CONFERENCE_LEFT: case CONFERENCE_LEFT:
case CONFERENCE_WILL_LEAVE: case CONFERENCE_WILL_LEAVE:
return _conferenceLeftOrWillLeave(state, action); return _conferenceLeftOrWillLeave(state, action);
@ -92,6 +97,9 @@ ReducerRegistry.register(
case SET_PASSWORD: case SET_PASSWORD:
return _setPassword(state, action); return _setPassword(state, action);
case SET_PENDING_SUBJECT_CHANGE:
return set(state, 'pendingSubjectChange', action.subject);
case SET_PREFERRED_RECEIVER_VIDEO_QUALITY: case SET_PREFERRED_RECEIVER_VIDEO_QUALITY:
return set( return set(
state, state,

View File

@ -31,6 +31,7 @@ import { maybeShowSuboptimalExperienceNotification } from '../../functions';
import Labels from './Labels'; import Labels from './Labels';
import { default as Notice } from './Notice'; import { default as Notice } from './Notice';
import { default as Subject } from './Subject';
declare var APP: Object; declare var APP: Object;
declare var config: Object; declare var config: Object;
@ -217,6 +218,7 @@ class Conference extends Component<Props> {
id = 'videoconference_page' id = 'videoconference_page'
onMouseMove = { this._onShowToolbar }> onMouseMove = { this._onShowToolbar }>
<Notice /> <Notice />
<Subject />
<div id = 'videospace'> <div id = 'videospace'>
<LargeVideo /> <LargeVideo />
{ hideVideoQualityLabel { hideVideoQualityLabel

View File

@ -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<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _subject, _visible } = this.props;
return (
<div className = { `subject ${_visible ? 'visible' : ''}` }>
{ _subject }
</div>
);
}
}
/**
* 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);

View File

@ -55,6 +55,7 @@ import {
setToolbarHovered setToolbarHovered
} from '../../actions'; } from '../../actions';
import AudioMuteButton from '../AudioMuteButton'; import AudioMuteButton from '../AudioMuteButton';
import { isToolboxVisible } from '../../functions';
import HangupButton from '../HangupButton'; import HangupButton from '../HangupButton';
import OverflowMenuButton from './OverflowMenuButton'; import OverflowMenuButton from './OverflowMenuButton';
import OverflowMenuProfileItem from './OverflowMenuProfileItem'; import OverflowMenuProfileItem from './OverflowMenuProfileItem';
@ -1281,11 +1282,8 @@ function _mapStateToProps(state) {
} = state['features/base/config']; } = state['features/base/config'];
const sharedVideoStatus = state['features/shared-video'].status; const sharedVideoStatus = state['features/shared-video'].status;
const { const {
alwaysVisible,
fullScreen, fullScreen,
overflowMenuVisible, overflowMenuVisible
timeoutID,
visible
} = state['features/toolbox']; } = state['features/toolbox'];
const localParticipant = getLocalParticipant(state); const localParticipant = getLocalParticipant(state);
const localRecordingStates = state['features/local-recording']; const localRecordingStates = state['features/local-recording'];
@ -1333,7 +1331,7 @@ function _mapStateToProps(state) {
_sharingVideo: sharedVideoStatus === 'playing' _sharingVideo: sharedVideoStatus === 'playing'
|| sharedVideoStatus === 'start' || sharedVideoStatus === 'start'
|| sharedVideoStatus === 'pause', || sharedVideoStatus === 'pause',
_visible: Boolean(timeoutID || visible || alwaysVisible), _visible: isToolboxVisible(state),
// XXX: We are not currently using state here, but in the future, when // XXX: We are not currently using state here, but in the future, when
// interfaceConfig is part of redux we will. // interfaceConfig is part of redux we will.

View File

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

View File

@ -1,3 +1,17 @@
// @flow // @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);
}

View File

@ -1,7 +1,5 @@
// @flow // @flow
export * from './functions.any';
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
/** /**
@ -26,3 +24,21 @@ export function getToolboxHeight() {
export function isButtonEnabled(name: string) { export function isButtonEnabled(name: string) {
return interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1; 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);
}