feat(shared-video) refactor dialog to use React

Also unify the mobile and web features into one, even though internally they still have separate ways to enable the functionality.
This commit is contained in:
Calinteodor 2021-03-03 16:37:38 +02:00 committed by GitHub
parent 8ee324b37f
commit 430591bd1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 594 additions and 459 deletions

View File

@ -125,7 +125,7 @@ import {
} from './react/features/prejoin';
import { disableReceiver, stopReceiver } from './react/features/remote-control';
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
import { setSharedVideoStatus } from './react/features/shared-video';
import { setSharedVideoStatus } from './react/features/shared-video/actions';
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
import { createPresenterEffect } from './react/features/stream-effects/presenter';
import { endpointMessageReceived } from './react/features/subtitles';

View File

@ -313,6 +313,7 @@
"unlockRoom": "Remove meeting $t(lockRoomPassword)",
"user": "user",
"userPassword": "user password",
"videoLink": "Video link",
"WaitForHostMsg": "The conference <b>{{room}}</b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
"WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
"WaitingForHost": "Waiting for the host ...",
@ -737,7 +738,7 @@
"remoteVideoMute": "Disable camera of participant",
"security": "Security options",
"Settings": "Toggle settings",
"sharedvideo": "Toggle Youtube video sharing",
"sharedvideo": "Toggle YouTube video sharing",
"shareRoom": "Invite someone",
"shareYourScreen": "Toggle screenshare",
"shortcuts": "Toggle shortcuts",

View File

@ -491,6 +491,25 @@ UI.onSharedVideoStop = function(id, attributes) {
}
};
/**
* Show shared video.
* @param {string} url video url
*/
UI.startSharedVideoEmitter = function(url) {
if (sharedVideoManager) {
sharedVideoManager.startSharedVideoEmitter(url);
}
};
/**
* Stop shared video.
*/
UI.stopSharedVideoEmitter = function() {
if (sharedVideoManager) {
sharedVideoManager.stopSharedVideoEmitter();
}
};
// TODO: Export every function separately. For now there is no point of doing
// this because we are importing everything.
export default UI;

View File

@ -12,11 +12,10 @@ import {
participantLeft,
pinParticipant
} from '../../../react/features/base/participants';
import { VIDEO_PLAYER_PARTICIPANT_NAME } from '../../../react/features/shared-video/constants';
import { dockToolbox, showToolbox } from '../../../react/features/toolbox/actions.web';
import { getToolboxHeight } from '../../../react/features/toolbox/functions.web';
import { YOUTUBE_PARTICIPANT_NAME } from '../../../react/features/youtube-player/constants';
import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../util/UIUtil';
import Filmstrip from '../videolayout/Filmstrip';
import LargeContainer from '../videolayout/LargeContainer';
import VideoLayout from '../videolayout/VideoLayout';
@ -29,14 +28,8 @@ export const SHARED_VIDEO_CONTAINER_TYPE = 'sharedvideo';
* Example shared video link.
* @type {string}
*/
const defaultSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
const updateInterval = 5000; // milliseconds
/**
* The dialog for user input (video link).
* @type {null}
*/
let dialog = null;
/**
* Manager of shared video.
@ -76,52 +69,37 @@ export default class SharedVideoManager {
}
/**
* Starts shared video by asking user for url, or if its already working
* asks whether the user wants to stop sharing the video.
* Start shared video event emitter if a video is not shown.
*
* @param url of the video
*/
toggleSharedVideo() {
if (dialog) {
return;
}
startSharedVideoEmitter(url) {
if (!this.isSharedVideoShown) {
requestVideoLink().then(
url => {
this.emitter.emit(
UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
sendAnalytics(createEvent('started'));
},
err => {
logger.log('SHARED VIDEO CANCELED', err);
sendAnalytics(createEvent('canceled'));
}
);
if (url) {
this.emitter.emit(
UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
sendAnalytics(createEvent('started'));
}
return;
logger.log('SHARED VIDEO CANCELED');
sendAnalytics(createEvent('canceled'));
}
}
/**
* Stop shared video event emitter done by the one who shared the video.
*/
stopSharedVideoEmitter() {
if (APP.conference.isLocalId(this.from)) {
showStopVideoPropmpt().then(
() => {
// make sure we stop updates for playing before we send stop
// if we stop it after receiving self presence, we can end
// up sending stop playing, and on the other end it will not
// stop
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
this.emitter.emit(
UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
sendAnalytics(createEvent('stopped'));
},
() => {}); // eslint-disable-line no-empty-function
} else {
APP.UI.messageHandler.showWarning({
descriptionKey: 'dialog.alreadySharedVideoMsg',
titleKey: 'dialog.alreadySharedVideoTitle'
});
sendAnalytics(createEvent('already.shared'));
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
this.emitter.emit(
UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
sendAnalytics(createEvent('stopped'));
}
}
@ -303,7 +281,7 @@ export default class SharedVideoManager {
conference: APP.conference._room,
id: self.url,
isFakeParticipant: true,
name: YOUTUBE_PARTICIPANT_NAME
name: VIDEO_PLAYER_PARTICIPANT_NAME
}));
APP.store.dispatch(pinParticipant(self.url));
@ -675,134 +653,3 @@ class SharedVideoContainer extends LargeContainer {
return false;
}
}
/**
* Checks if given string is youtube url.
* @param {string} url string to check.
* @returns {boolean}
*/
function getYoutubeLink(url) {
const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
return url.match(p) ? RegExp.$1 : false;
}
/**
* Ask user if he want to close shared video.
*/
function showStopVideoPropmpt() {
return new Promise((resolve, reject) => {
const submitFunction = function(e, v) {
if (v) {
resolve();
} else {
reject();
}
};
const closeFunction = function() {
dialog = null;
};
dialog = APP.UI.messageHandler.openTwoButtonDialog({
titleKey: 'dialog.removeSharedVideoTitle',
msgKey: 'dialog.removeSharedVideoMsg',
leftButtonKey: 'dialog.Remove',
submitFunction,
closeFunction
});
});
}
/**
* Ask user for shared video url to share with others.
* Dialog validates client input to allow only youtube urls.
*/
function requestVideoLink() {
const i18n = APP.translation;
const cancelButton = i18n.generateTranslationHTML('dialog.Cancel');
const shareButton = i18n.generateTranslationHTML('dialog.Share');
const backButton = i18n.generateTranslationHTML('dialog.Back');
const linkError
= i18n.generateTranslationHTML('dialog.shareVideoLinkError');
return new Promise((resolve, reject) => {
dialog = APP.UI.messageHandler.openDialogWithStates({
state0: {
titleKey: 'dialog.shareVideoTitle',
html: `
<input name='sharedVideoUrl' type='text'
class='input-control'
data-i18n='[placeholder]defaultLink'
autofocus>`,
persistent: false,
buttons: [
{ title: cancelButton,
value: false },
{ title: shareButton,
value: true }
],
focus: ':input:first',
defaultButton: 1,
submit(e, v, m, f) { // eslint-disable-line max-params
e.preventDefault();
if (!v) {
reject('cancelled');
dialog.close();
return;
}
const sharedVideoUrl = f.sharedVideoUrl;
if (!sharedVideoUrl) {
return;
}
const urlValue
= encodeURI(UIUtil.escapeHtml(sharedVideoUrl));
const yVideoId = getYoutubeLink(urlValue);
if (!yVideoId) {
dialog.goToState('state1');
return false;
}
resolve(yVideoId);
dialog.close();
}
},
state1: {
titleKey: 'dialog.shareVideoTitle',
html: linkError,
persistent: false,
buttons: [
{ title: cancelButton,
value: false },
{ title: backButton,
value: true }
],
focus: ':input:first',
defaultButton: 1,
submit(e, v) {
e.preventDefault();
if (v === 0) {
reject();
dialog.close();
} else {
dialog.goToState('state0');
}
}
}
}, {
close() {
dialog = null;
}
}, {
url: defaultSharedVideoLink
});
});
}

View File

@ -13,6 +13,6 @@ import '../mobile/proximity/middleware';
import '../mobile/wake-lock/middleware';
import '../mobile/watchos/middleware';
import '../share-room/middleware';
import '../youtube-player/middleware';
import '../shared-video/middleware';
import './middlewares.any';

View File

@ -8,6 +8,6 @@ import '../mobile/external-api/reducer';
import '../mobile/full-screen/reducer';
import '../mobile/incoming-call/reducer';
import '../mobile/watchos/reducer';
import '../youtube-player/reducer';
import '../shared-video/reducer';
import './reducers.any';

View File

@ -11,7 +11,7 @@ import { toState } from '../redux';
* @param {string} flag - The name of the React {@code Component} prop of
* the currently mounted {@code App} to get.
* @param {*} defaultValue - A default value for the flag, in case it's not defined.
* @returns {*} The value of the specified React {@code Compoennt} prop of the
* @returns {*} The value of the specified React {@code Component} prop of the
* currently mounted {@code App}.
*/
export function getFeatureFlag(stateful: Function | Object, flag: string, defaultValue: any) {

View File

@ -3,7 +3,7 @@
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { YoutubeLargeVideo } from '../../../youtube-player/components';
import { YoutubeLargeVideo } from '../../../shared-video/components';
import { Avatar } from '../../avatar';
import { translate } from '../../i18n';
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';

View File

@ -1,6 +1,8 @@
// @flow
/**
* The type of the action which signals to update the current known state of the
* shared YouTube video.
* shared video.
*
* {
* type: SET_SHARED_VIDEO_STATUS,
@ -11,10 +13,21 @@ export const SET_SHARED_VIDEO_STATUS = 'SET_SHARED_VIDEO_STATUS';
/**
* The type of the action which signals to start the flow for starting or
* stopping a shared YouTube video.
* stopping a shared video.
*
* {
* type: TOGGLE_SHARED_VIDEO
* }
*/
export const TOGGLE_SHARED_VIDEO = 'TOGGLE_SHARED_VIDEO';
/**
* The type of the action which signals to disable or enable the shared video
* button.
*
* {
* type: SET_DISABLE_BUTTON
* }
*/
export const SET_DISABLE_BUTTON = 'SET_DISABLE_BUTTON';

View File

@ -1,31 +0,0 @@
import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO } from './actionTypes';
/**
* Updates the current known status of the shared YouTube video.
*
* @param {string} status - The current status of the YouTube video being
* shared.
* @returns {{
* type: SET_SHARED_VIDEO_STATUS,
* status: string
* }}
*/
export function setSharedVideoStatus(status) {
return {
type: SET_SHARED_VIDEO_STATUS,
status
};
}
/**
* Starts the flow for starting or stopping a shared YouTube video.
*
* @returns {{
* type: TOGGLE_SHARED_VIDEO
* }}
*/
export function toggleSharedVideo() {
return {
type: TOGGLE_SHARED_VIDEO
};
}

View File

@ -2,16 +2,16 @@
import { openDialog } from '../base/dialog';
import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
import { EnterVideoLinkPrompt } from './components';
import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO } from './actionTypes';
import { SharedVideoDialog } from './components/native';
/**
* Updates the current known status of the shared YouTube video.
* Updates the current known status of the shared video.
*
* @param {string} videoId - The youtubeId of the video to be shared.
* @param {string} status - The current status of the YouTube video being shared.
* @param {number} time - The current position of the YouTube video being shared.
* @param {string} ownerId - The participantId of the user sharing the YouTube video.
* @param {string} videoId - The id of the video to be shared.
* @param {string} status - The current status of the video being shared.
* @param {number} time - The current position of the video being shared.
* @param {string} ownerId - The participantId of the user sharing the video.
* @returns {{
* type: SET_SHARED_VIDEO_STATUS,
* ownerId: string,
@ -31,7 +31,7 @@ export function setSharedVideoStatus(videoId: string, status: string, time: numb
}
/**
* Starts the flow for starting or stopping a shared YouTube video.
* Starts the flow for starting or stopping a shared video.
*
* @returns {{
* type: TOGGLE_SHARED_VIDEO
@ -39,16 +39,16 @@ export function setSharedVideoStatus(videoId: string, status: string, time: numb
*/
export function toggleSharedVideo() {
return {
type: 'TOGGLE_SHARED_VIDEO'
type: TOGGLE_SHARED_VIDEO
};
}
/**
* Displays the prompt for entering the youtube video link.
* Displays the prompt for entering the video link.
*
* @param {Function} onPostSubmit - The function to be invoked when a valid link is entered.
* @returns {Function}
*/
export function showEnterVideoLinkPrompt(onPostSubmit: ?Function) {
return openDialog(EnterVideoLinkPrompt, { onPostSubmit });
export function showSharedVideoDialog(onPostSubmit: ?Function) {
return openDialog(SharedVideoDialog, { onPostSubmit });
}

View File

@ -0,0 +1,62 @@
// @flow
import { openDialog } from '../base/dialog/actions';
import { SharedVideoDialog } from '../shared-video/components';
import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO, SET_DISABLE_BUTTON } from './actionTypes';
/**
* Updates the current known status of the shared video.
*
* @param {string} status - The current status of the video being shared.
* @returns {{
* type: SET_SHARED_VIDEO_STATUS,
* status: string
* }}
*/
export function setSharedVideoStatus(status: string) {
return {
type: SET_SHARED_VIDEO_STATUS,
status
};
}
/**
* Disabled share video button.
*
* @param {boolean} disabled - The current state of the share video button.
* @returns {{
* type: SET_DISABLE_BUTTON,
* disabled: boolean
* }}
*/
export function setDisableButton(disabled: boolean) {
return {
type: SET_DISABLE_BUTTON,
disabled
};
}
/**
* Starts the flow for starting or stopping a shared video.
*
* @returns {{
* type: TOGGLE_SHARED_VIDEO
* }}
*/
export function toggleSharedVideo() {
return {
type: TOGGLE_SHARED_VIDEO
};
}
/**
* Displays the dialog for entering the video link.
*
* @param {Function} onPostSubmit - The function to be invoked when a valid link is entered.
* @returns {Function}
*/
export function showSharedVideoDialog(onPostSubmit: ?Function) {
return openDialog(SharedVideoDialog, { onPostSubmit });
}

View File

@ -3,31 +3,38 @@
import { Component } from 'react';
import type { Dispatch } from 'redux';
import { getYoutubeLink } from '../functions';
/**
* The type of the React {@code Component} props of
* {@link AbstractEnterVideoLinkPrompt}.
* {@link AbstractSharedVideoDialog}.
*/
export type Props = {
/**
* Invoked to update the shared youtube video link.
* Invoked to update the shared video link.
*/
dispatch: Dispatch<any>,
/**
* Function to be invoked after typing a valid youtube video .
* Function to be invoked after typing a valid video.
*/
onPostSubmit: ?Function
onPostSubmit: ?Function,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* Implements an abstract class for {@code EnterVideoLinkPrompt}.
* Implements an abstract class for {@code SharedVideoDialog}.
*/
export default class AbstractEnterVideoLinkPrompt<S: *> extends Component < Props, S > {
export default class AbstractSharedVideoDialog<S: *> extends Component < Props, S > {
/**
* Instantiates a new component.
*
*
* @inheritdoc
*/
constructor(props: Props) {
@ -39,7 +46,7 @@ export default class AbstractEnterVideoLinkPrompt<S: *> extends Component < Prop
_onSetVideoLink: string => boolean;
/**
* Validates the entered video link by extractibg the id and dispatches it.
* Validates the entered video link by extracting the id and dispatches it.
*
* It returns a boolean to comply the Dialog behaviour:
* {@code true} - the dialog should be closed.
@ -48,7 +55,7 @@ export default class AbstractEnterVideoLinkPrompt<S: *> extends Component < Prop
* @param {string} link - The entered video link.
* @returns {boolean}
*/
_onSetVideoLink(link) {
_onSetVideoLink(link: string) {
if (!link || !link.trim()) {
return false;
}
@ -67,17 +74,4 @@ export default class AbstractEnterVideoLinkPrompt<S: *> extends Component < Prop
}
}
/**
* Validates the entered video url.
*
* It returns a boolean to reflect whether the url matches the youtube regex.
*
* @param {string} url - The entered video link.
* @returns {boolean}
*/
function getYoutubeLink(url) {
const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|(?:m\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
const result = url.match(p);
return result ? result[1] : false;
}

View File

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

View File

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

View File

@ -2,13 +2,14 @@
import type { Dispatch } from 'redux';
import { getFeatureFlag, VIDEO_SHARE_BUTTON_ENABLED } from '../../base/flags';
import { translate } from '../../base/i18n';
import { IconShareVideo } from '../../base/icons';
import { getLocalParticipant } from '../../base/participants';
import { connect } from '../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { toggleSharedVideo } from '../actions';
import { getFeatureFlag, VIDEO_SHARE_BUTTON_ENABLED } from '../../../base/flags';
import { translate } from '../../../base/i18n';
import { IconShareVideo } from '../../../base/icons';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { toggleSharedVideo } from '../../actions.native';
import { isSharingStatus } from '../../functions';
/**
* The type of the React {@code Component} props of {@link TileViewButton}.
@ -21,7 +22,7 @@ type Props = AbstractButtonProps & {
_isDisabled: boolean,
/**
* Whether or not the local participant is sharing a YouTube video.
* Whether or not the local participant is sharing a video.
*/
_sharingVideo: boolean,
@ -76,7 +77,7 @@ class VideoShareButton extends AbstractButton<Props, *> {
}
/**
* Dispatches an action to toggle YouTube video sharing.
* Dispatches an action to toggle video sharing.
*
* @private
* @returns {void}
@ -95,7 +96,7 @@ class VideoShareButton extends AbstractButton<Props, *> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps): Object {
const { ownerId, status: sharedVideoStatus } = state['features/youtube-player'];
const { ownerId, status: sharedVideoStatus } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state).id;
const enabled = getFeatureFlag(state, VIDEO_SHARE_BUTTON_ENABLED, true);
const { visible = enabled } = ownProps;
@ -104,24 +105,15 @@ function _mapStateToProps(state, ownProps): Object {
return {
_isDisabled: isSharingStatus(sharedVideoStatus),
_sharingVideo: false,
visible };
visible
};
}
return {
_isDisabled: false,
_sharingVideo: isSharingStatus(sharedVideoStatus),
visible
};
}
/**
* Checks if the status is one that is actually sharing the video - playing, pause or start.
*
* @param {string} status - The shared video status.
* @private
* @returns {boolean}
*/
function isSharingStatus(status) {
return [ 'playing', 'pause', 'start' ].includes(status);
}
export default translate(connect(_mapStateToProps)(VideoShareButton));

View File

@ -4,12 +4,13 @@ import React from 'react';
import { InputDialog } from '../../../base/dialog';
import { connect } from '../../../base/redux';
import AbstractEnterVideoLinkPrompt from '../AbstractEnterVideoLinkPrompt';
import { defaultSharedVideoLink } from '../../constants';
import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
/**
* Implements a component to render a display name prompt.
*/
class EnterVideoLinkPrompt extends AbstractEnterVideoLinkPrompt<*> {
class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
/**
* Implements React's {@link Component#render()}.
*
@ -21,7 +22,7 @@ class EnterVideoLinkPrompt extends AbstractEnterVideoLinkPrompt<*> {
contentKey = 'dialog.shareVideoTitle'
onSubmit = { this._onSetVideoLink }
textInputProps = {{
placeholder: 'https://youtu.be/TB7LlM4erx8'
placeholder: defaultSharedVideoLink
}} />
);
}
@ -29,4 +30,4 @@ class EnterVideoLinkPrompt extends AbstractEnterVideoLinkPrompt<*> {
_onSetVideoLink: string => boolean;
}
export default connect()(EnterVideoLinkPrompt);
export default connect()(SharedVideoDialog);

View File

@ -6,9 +6,9 @@ import YoutubePlayer from 'react-native-youtube-iframe';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui';
import { setToolboxVisible } from '../../../toolbox/actions';
import { setSharedVideoStatus } from '../../actions';
import { setSharedVideoStatus } from '../../actions.native';
import styles from './styles';
@ -383,7 +383,7 @@ function shouldSeekToPosition(newTime, previousTime) {
* @returns {Props}
*/
function _mapStateToProps(state) {
const { ownerId, status, time } = state['features/youtube-player'];
const { ownerId, status, time } = state['features/shared-video'];
const localParticipant = getLocalParticipant(state);
const responsiveUi = state['features/base/responsive-ui'];
const { aspectRatio, clientHeight: screenHeight, clientWidth: screenWidth } = responsiveUi;

View File

@ -0,0 +1,6 @@
// @flow
export { default as SharedVideoButton } from './SharedVideoButton';
export { default as SharedVideoDialog } from './SharedVideoDialog';
export { default as YoutubeLargeVideo } from './YoutubeLargeVideo';

View File

@ -0,0 +1,112 @@
// @flow
import type { Dispatch } from 'redux';
import { translate } from '../../../base/i18n';
import { IconShareVideo } from '../../../base/icons';
import { connect } from '../../../base/redux';
import {
AbstractButton,
type AbstractButtonProps
} from '../../../base/toolbox/components';
import { showSharedVideoDialog } from '../../actions.web';
import { isSharingStatus } from '../../functions';
declare var APP: Object;
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Dispatch<any>,
/**
* Whether or not the button is disabled.
*/
_isDisabled: boolean,
/**
* Whether or not the local participant is sharing a video.
*/
_sharingVideo: boolean
};
/**
* Implements an {@link AbstractButton} to open the user documentation in a new window.
*/
class SharedVideoButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.sharedvideo';
icon = IconShareVideo;
label = 'toolbar.sharedvideo';
tooltip = 'toolbar.sharedvideo';
toggledLabel = 'toolbar.stopSharedVideo';
/**
* Handles clicking / pressing the button, and opens a new dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
this._doToggleSharedVideoDialog();
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props._sharingVideo;
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._isDisabled;
}
/**
* Dispatches an action to toggle video sharing.
*
* @private
* @returns {void}
*/
_doToggleSharedVideoDialog() {
const { dispatch } = this.props;
return this._isToggled()
? APP.UI.stopSharedVideoEmitter()
: dispatch(showSharedVideoDialog(id => APP.UI.startSharedVideoEmitter(id)));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state): Object {
const {
disabled: sharedVideoBtnDisabled,
status: sharedVideoStatus
} = state['features/shared-video'];
return {
_isDisabled: sharedVideoBtnDisabled,
_sharingVideo: isSharingStatus(sharedVideoStatus)
};
}
export default translate(connect(_mapStateToProps)(SharedVideoButton));

View File

@ -0,0 +1,102 @@
// @flow
import { FieldTextStateless } from '@atlaskit/field-text';
import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { getFieldValue } from '../../../base/react';
import { connect } from '../../../base/redux';
import { defaultSharedVideoLink } from '../../constants';
import { getYoutubeLink } from '../../functions';
import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
/**
* Component that renders the video share dialog.
*
* @returns {React$Element<any>}
*/
class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props) {
super(props);
this.state = {
value: '',
okDisabled: true
};
this._onChange = this._onChange.bind(this);
this._onSubmitValue = this._onSubmitValue.bind(this);
}
_onChange: Object => void;
/**
* Callback for the onChange event of the field.
*
* @param {Object} evt - The static event.
* @returns {void}
*/
_onChange(evt: Object) {
const linkValue = getFieldValue(evt);
this.setState({
value: linkValue,
okDisabled: !getYoutubeLink(linkValue)
});
}
_onSubmitValue: () => boolean;
/**
* Callback to be invoked when the value of the link input is submitted.
*
* @returns {boolean}
*/
_onSubmitValue() {
return this._onSetVideoLink(this.state.value);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const { t } = this.props;
return (
<Dialog
hideCancelButton = { false }
okDisabled = { this.state.okDisabled }
okKey = { t('dialog.Share') }
onSubmit = { this._onSubmitValue }
titleKey = { t('dialog.shareVideoTitle') }
width = { 'small' }>
<FieldTextStateless
autoFocus = { true }
className = 'input-control'
compact = { false }
label = { t('dialog.videoLink') }
name = 'sharedVideoUrl'
onChange = { this._onChange }
placeholder = { defaultSharedVideoLink }
shouldFitContainer = { true }
type = 'text'
value = { this.state.value } />
</Dialog>
);
}
_onSetVideoLink: string => boolean;
_onChange: Object => void;
}
export default translate(connect()(SharedVideoDialog));

View File

@ -0,0 +1,5 @@
// @flow
export { default as SharedVideoButton } from './SharedVideoButton';
export { default as SharedVideoDialog } from './SharedVideoDialog';

View File

@ -0,0 +1,19 @@
// @flow
/**
* Example shared video link.
* @type {string}
*/
export const defaultSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
/**
* Fixed name of the video player fake participant.
* @type {string}
*/
export const VIDEO_PLAYER_PARTICIPANT_NAME = 'YouTube';
/**
* Shared video command.
* @type {string}
*/
export const SHARED_VIDEO = 'shared-video';

View File

@ -0,0 +1,44 @@
// @flow
import { getParticipants } from '../base/participants';
import { VIDEO_PLAYER_PARTICIPANT_NAME } from './constants';
/**
* Validates the entered video url.
*
* It returns a boolean to reflect whether the url matches the youtube regex.
*
* @param {string} url - The entered video link.
* @returns {boolean}
*/
export function getYoutubeLink(url: string) {
const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|(?:m\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
const result = url.match(p);
return result ? result[1] : false;
}
/**
* Checks if the status is one that is actually sharing the video - playing, pause or start.
*
* @param {string} status - The shared video status.
* @returns {boolean}
*/
export function isSharingStatus(status: string) {
return [ 'playing', 'pause', 'start' ].includes(status);
}
/**
* Returns true if there is a video being shared in the meeting.
*
* @param {Object | Function} stateful - The Redux state or a function that gets resolved to the Redux state.
* @returns {boolean}
*/
export function isVideoPlaying(stateful: Object | Function): boolean {
return Boolean(getParticipants(stateful).find(p => p.isFakeParticipant
&& p.name === VIDEO_PLAYER_PARTICIPANT_NAME)
);
}

View File

@ -1,2 +0,0 @@
export * from './actions';
export * from './actionTypes';

View File

@ -1,30 +0,0 @@
// @flow
import UIEvents from '../../../service/UI/UIEvents';
import { MiddlewareRegistry } from '../base/redux';
import { TOGGLE_SHARED_VIDEO } from './actionTypes';
declare var APP: Object;
/**
* Middleware that captures actions related to YouTube video sharing and updates
* components not hooked into redux.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
if (typeof APP === 'undefined') {
return next(action);
}
switch (action.type) {
case TOGGLE_SHARED_VIDEO:
APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
break;
}
return next(action);
});

View File

@ -11,13 +11,12 @@ import {
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { TOGGLE_SHARED_VIDEO, SET_SHARED_VIDEO_STATUS } from './actionTypes';
import { setSharedVideoStatus, showEnterVideoLinkPrompt } from './actions';
import { YOUTUBE_PARTICIPANT_NAME } from './constants';
const SHARED_VIDEO = 'shared-video';
import { setSharedVideoStatus, showSharedVideoDialog } from './actions.native';
import { SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME } from './constants';
import { isSharingStatus } from './functions';
/**
* Middleware that captures actions related to YouTube video sharing and updates
* Middleware that captures actions related to video sharing and updates
* components not hooked into redux.
*
* @param {Store} store - The redux store.
@ -29,7 +28,7 @@ MiddlewareRegistry.register(store => next => action => {
const conference = getCurrentConference(state);
const localParticipantId = getLocalParticipant(state)?.id;
const { videoId, status, ownerId, time } = action;
const { ownerId: stateOwnerId, videoId: stateVideoId } = state['features/youtube-player'];
const { ownerId: stateOwnerId, videoId: stateVideoId } = state['features/shared-video'];
switch (action.type) {
case TOGGLE_SHARED_VIDEO:
@ -71,7 +70,7 @@ StateListenerRegistry.register(
const localParticipantId = getLocalParticipant(getState()).id;
const status = attributes.state;
if ([ 'playing', 'pause', 'start' ].includes(status)) {
if (isSharingStatus(status)) {
handleSharingVideoStatus(store, value, attributes, conference);
} else if (status === 'stop') {
dispatch(participantLeft(value, conference));
@ -82,7 +81,8 @@ StateListenerRegistry.register(
}
);
}
});
}
);
/**
* Handles the playing, pause and start statuses for the shared video.
@ -90,7 +90,7 @@ StateListenerRegistry.register(
* Sets the SharedVideoStatus if the event was triggered by the local user.
*
* @param {Store} store - The redux store.
* @param {string} videoId - The YoutubeId of the video to the shared.
* @param {string} videoId - The id of the video to the shared.
* @param {Object} attributes - The attributes received from the share video command.
* @param {JitsiConference} conference - The current conference.
* @returns {void}
@ -98,7 +98,7 @@ StateListenerRegistry.register(
function handleSharingVideoStatus(store, videoId, { state, time, from }, conference) {
const { dispatch, getState } = store;
const localParticipantId = getLocalParticipant(getState()).id;
const oldStatus = getState()['features/youtube-player']?.status;
const oldStatus = getState()['features/shared-video']?.status;
if (state === 'start' || ![ 'playing', 'pause', 'start' ].includes(oldStatus)) {
dispatch(participantJoined({
@ -106,7 +106,7 @@ function handleSharingVideoStatus(store, videoId, { state, time, from }, confere
id: videoId,
isFakeParticipant: true,
avatarURL: `https://img.youtube.com/vi/${videoId}/0.jpg`,
name: YOUTUBE_PARTICIPANT_NAME
name: VIDEO_PLAYER_PARTICIPANT_NAME
}));
dispatch(pinParticipant(videoId));
@ -130,7 +130,7 @@ function handleSharingVideoStatus(store, videoId, { state, time, from }, confere
function _toggleSharedVideo(store, next, action) {
const { dispatch, getState } = store;
const state = getState();
const { videoId, ownerId, status } = state['features/youtube-player'];
const { videoId, ownerId, status } = state['features/shared-video'];
const localParticipant = getLocalParticipant(state);
if (status === 'playing' || status === 'start' || status === 'pause') {
@ -138,7 +138,7 @@ function _toggleSharedVideo(store, next, action) {
dispatch(setSharedVideoStatus(videoId, 'stop', 0, localParticipant.id));
}
} else {
dispatch(showEnterVideoLinkPrompt(id => _onVideoLinkEntered(store, id)));
dispatch(showSharedVideoDialog(id => _onVideoLinkEntered(store, id)));
}
return next(action);
@ -148,7 +148,7 @@ function _toggleSharedVideo(store, next, action) {
* Sends SHARED_VIDEO start command.
*
* @param {Store} store - The redux store.
* @param {string} id - The youtube id of the video to be shared.
* @param {string} id - The id of the video to be shared.
* @returns {void}
*/
function _onVideoLinkEntered(store, id) {
@ -167,7 +167,7 @@ function _onVideoLinkEntered(store, id) {
/**
* Sends SHARED_VIDEO command.
*
* @param {string} id - The youtube id of the video.
* @param {string} id - The id of the video.
* @param {string} status - The status of the shared video.
* @param {JitsiConference} conference - The current conference.
* @param {string} localParticipantId - The id of the local participant.

View File

@ -0,0 +1,63 @@
// @flow
import UIEvents from '../../../service/UI/UIEvents';
import { getCurrentConference } from '../base/conference';
import { getLocalParticipant } from '../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { TOGGLE_SHARED_VIDEO } from './actionTypes';
import { setDisableButton } from './actions.web';
import { SHARED_VIDEO } from './constants';
declare var APP: Object;
/**
* Middleware that captures actions related to video sharing and updates
* components not hooked into redux.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
if (typeof APP === 'undefined') {
return next(action);
}
switch (action.type) {
case TOGGLE_SHARED_VIDEO:
APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
break;
}
return next(action);
});
/**
* Set up state change listener to disable or enable the share video button in
* the toolbar menu.
*/
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, store, previousConference) => {
if (conference && conference !== previousConference) {
conference.addCommandListener(SHARED_VIDEO,
({ attributes }) => {
const { dispatch, getState } = store;
const { from } = attributes;
const localParticipantId = getLocalParticipant(getState()).id;
const status = attributes.state;
if (status === 'playing') {
if (localParticipantId !== from) {
dispatch(setDisableButton(true));
}
} else if (status === 'stop') {
dispatch(setDisableButton(false));
}
}
);
}
}
);

View File

@ -1,3 +1,5 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
@ -6,13 +8,17 @@ import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
* Reduces the Redux actions of the feature features/shared-video.
*/
ReducerRegistry.register('features/shared-video', (state = {}, action) => {
const { videoId, status, time, ownerId } = action;
switch (action.type) {
case SET_SHARED_VIDEO_STATUS:
return {
...state,
status: action.status
videoId,
status,
time,
ownerId
};
default:
return state;
}

View File

@ -0,0 +1,29 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { SET_SHARED_VIDEO_STATUS, SET_DISABLE_BUTTON } from './actionTypes';
/**
* Reduces the Redux actions of the feature features/shared-video.
*/
ReducerRegistry.register('features/shared-video', (state = {}, action) => {
const { status, disabled } = action;
switch (action.type) {
case SET_SHARED_VIDEO_STATUS:
return {
...state,
status
};
case SET_DISABLE_BUTTON:
return {
...state,
disabled
};
default:
return state;
}
});

View File

@ -15,9 +15,9 @@ import { LobbyModeButton } from '../../../lobby/components/native';
import { AudioRouteButton } from '../../../mobile/audio-mode';
import { LiveStreamButton, RecordButton } from '../../../recording';
import { RoomLockButton } from '../../../room-lock';
import { SharedVideoButton } from '../../../shared-video/components';
import { ClosedCaptionButton } from '../../../subtitles';
import { TileViewButton } from '../../../video-layout';
import { VideoShareButton } from '../../../youtube-player/components';
import HelpButton from '../HelpButton';
import MuteEveryoneButton from '../MuteEveryoneButton';
@ -140,7 +140,7 @@ class OverflowMenu extends PureComponent<Props, State> {
<TileViewButton { ...buttonProps } />
<RecordButton { ...buttonProps } />
<LiveStreamButton { ...buttonProps } />
<VideoShareButton { ...buttonProps } />
<SharedVideoButton { ...buttonProps } />
<RoomLockButton { ...buttonProps } />
<ClosedCaptionButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } />

View File

@ -22,8 +22,7 @@ import {
IconPresentation,
IconRaisedHand,
IconRec,
IconShareDesktop,
IconShareVideo
IconShareDesktop
} from '../../../base/icons';
import JitsiMeetJS from '../../../base/lib-jitsi-meet';
import {
@ -57,7 +56,7 @@ import {
SettingsButton,
openSettingsDialog
} from '../../../settings';
import { toggleSharedVideo } from '../../../shared-video';
import { SharedVideoButton } from '../../../shared-video/components';
import { SpeakerStats } from '../../../speaker-stats';
import {
ClosedCaptionButton
@ -90,6 +89,7 @@ import OverflowMenuProfileItem from './OverflowMenuProfileItem';
import ToolbarButton from './ToolbarButton';
import VideoSettingsButton from './VideoSettingsButton';
/**
* The type of the React {@code Component} props of {@link Toolbox}.
*/
@ -259,7 +259,6 @@ class Toolbox extends Component<Props, State> {
this._onToolbarToggleProfile = this._onToolbarToggleProfile.bind(this);
this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
this._onToolbarToggleSharedVideo = this._onToolbarToggleSharedVideo.bind(this);
this._onToolbarOpenLocalRecordingInfoDialog = this._onToolbarOpenLocalRecordingInfoDialog.bind(this);
this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this);
@ -504,16 +503,6 @@ class Toolbox extends Component<Props, State> {
}
}
/**
* Dispatches an action to toggle YouTube video sharing.
*
* @private
* @returns {void}
*/
_doToggleSharedVideo() {
this.props.dispatch(toggleSharedVideo());
}
/**
* Dispatches an action to toggle the video quality dialog.
*
@ -897,24 +886,6 @@ class Toolbox extends Component<Props, State> {
this._doToggleScreenshare();
}
_onToolbarToggleSharedVideo: () => void;
/**
* Creates an analytics toolbar event and dispatches an action for toggling
* the sharing of a YouTube video.
*
* @private
* @returns {void}
*/
_onToolbarToggleSharedVideo() {
sendAnalytics(createToolbarEvent('shared.video.toggled',
{
enable: !this.props._sharingVideo
}));
this._doToggleSharedVideo();
}
_onToolbarOpenLocalRecordingInfoDialog: () => void;
/**
@ -930,7 +901,7 @@ class Toolbox extends Component<Props, State> {
}
/**
* Returns true if the the desktop sharing button should be visible and
* Returns true if the desktop sharing button should be visible and
* false otherwise.
*
* @returns {boolean}
@ -1028,7 +999,6 @@ class Toolbox extends Component<Props, State> {
_feedbackConfigured,
_fullScreen,
_screensharing,
_sharingVideo,
t
} = this.props;
@ -1057,12 +1027,9 @@ class Toolbox extends Component<Props, State> {
key = 'record'
showLabel = { true } />,
this._shouldShowButton('sharedvideo')
&& <OverflowMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.sharedvideo') }
icon = { IconShareVideo }
&& <SharedVideoButton
key = 'sharedvideo'
onClick = { this._onToolbarToggleSharedVideo }
text = { _sharingVideo ? t('toolbar.stopSharedVideo') : t('toolbar.sharedvideo') } />,
showLabel = { true } />,
this._shouldShowButton('etherpad')
&& <SharedDocumentButton
key = 'etherpad'
@ -1435,7 +1402,6 @@ function _mapStateToProps(state) {
callStatsID,
enableFeaturesBasedOnToken
} = state['features/base/config'];
const sharedVideoStatus = state['features/shared-video'].status;
const {
fullScreen,
overflowMenuVisible
@ -1476,9 +1442,6 @@ function _mapStateToProps(state) {
_overflowMenuVisible: overflowMenuVisible,
_raisedHand: localParticipant.raisedHand,
_screensharing: localVideo && localVideo.videoType === 'desktop',
_sharingVideo: sharedVideoStatus === 'playing'
|| sharedVideoStatus === 'start'
|| sharedVideoStatus === 'pause',
_visible: isToolboxVisible(state),
_visibleButtons: equals(visibleButtons, buttons) ? visibleButtons : buttons
};

View File

@ -10,7 +10,7 @@ import {
SINGLE_COLUMN_BREAKPOINT,
TWO_COLUMN_BREAKPOINT
} from '../filmstrip/constants';
import { isYoutubeVideoPlaying } from '../youtube-player/functions';
import { isVideoPlaying } from '../shared-video/functions';
import { LAYOUTS } from './constants';
@ -154,7 +154,7 @@ export function shouldDisplayTileView(state: Object = {}) {
|| participantCount < 3
// There is a shared YouTube video in the meeting
|| isYoutubeVideoPlaying(state)
|| isVideoPlaying(state)
// We want jibri to use stage view by default
|| iAmRecorder

View File

@ -1,22 +0,0 @@
/**
* The type of the action which signals to update the current known state of the
* shared YouTube video.
*
* {
* type: SET_SHARED_VIDEO_STATUS,
* status: string,
* time: string,
* ownerId: string
* }
*/
export const SET_SHARED_VIDEO_STATUS = 'SET_SHARED_VIDEO_STATUS';
/**
* The type of the action which signals to start the flow for starting or
* stopping a shared YouTube video.
*
* {
* type: TOGGLE_SHARED_VIDEO
* }
*/
export const TOGGLE_SHARED_VIDEO = 'TOGGLE_SHARED_VIDEO';

View File

@ -1,6 +0,0 @@
// @flow
import { Component } from 'react';
export { Component as EnterVideoLinkPrompt };
export { Component as YoutubeLargeVideo };

View File

@ -1,5 +0,0 @@
// @flow
export { default as VideoShareButton } from './VideoShareButton';
export * from './_';

View File

@ -1,4 +0,0 @@
// @flow
export { default as EnterVideoLinkPrompt } from './EnterVideoLinkPrompt';
export { default as YoutubeLargeVideo } from './YoutubeLargeVideo';

View File

@ -1,6 +0,0 @@
// @flow
/**
* Fixed name of the YouTube player fake participant.
*/
export const YOUTUBE_PARTICIPANT_NAME = 'YouTube';

View File

@ -1,15 +0,0 @@
// @flow
import { getParticipants } from '../base/participants';
import { YOUTUBE_PARTICIPANT_NAME } from './constants';
/**
* Returns true if there is a youtube video being shaerd in the meeting.
*
* @param {Object | Function} stateful - The Redux state or a function that gets resolved to the Redux state.
* @returns {boolean}
*/
export function isYoutubeVideoPlaying(stateful: Object | Function): boolean {
return Boolean(getParticipants(stateful).find(p => p.isFakeParticipant && p.name === YOUTUBE_PARTICIPANT_NAME));
}

View File

@ -1,24 +0,0 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
/**
* Reduces the Redux actions of the feature features/youtube-player.
*/
ReducerRegistry.register('features/youtube-player', (state = {}, action) => {
const { videoId, status, time, ownerId } = action;
switch (action.type) {
case SET_SHARED_VIDEO_STATUS:
return {
...state,
videoId,
status,
time,
ownerId
};
default:
return state;
}
});