Skip to content

Commit

Permalink
feat(shared-video): Shows confirmation dialog before playing video. (#…
Browse files Browse the repository at this point in the history
…15059)

* feat(shared-video): Shows confirmation dialog before playing video.

* feat(shared-video/native): created ShareVideoConfirmDialog and unified actions

* squash: Simplifies state and fixes stop and then start scenario.

* squash: Use constants everywhere.

* squash: Use helper function.

* squash: Ignore any command with not matching video URL.

---------

Co-authored-by: Calin-Teodor <[email protected]>
  • Loading branch information
damencho and Calinteodor authored Aug 27, 2024
1 parent 5b4383d commit 3f7c3b8
Show file tree
Hide file tree
Showing 15 changed files with 210 additions and 35 deletions.
2 changes: 2 additions & 0 deletions lang/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,8 @@
"shareScreenWarningD2": "you need to stop audio sharing, start screen sharing and check the \"share audio\" option.",
"shareScreenWarningH1": "If you want to share just your screen:",
"shareScreenWarningTitle": "You need to stop audio sharing before sharing your screen",
"shareVideoConfirmPlay": "You’re about to open an external website. Do you want to continue?",
"shareVideoConfirmPlayTitle": "{{name}} has shared a video with you.",
"shareVideoLinkError": "Please provide a correct video link.",
"shareVideoTitle": "Share video",
"shareYourScreen": "Share your screen",
Expand Down
2 changes: 1 addition & 1 deletion modules/API/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ function initCommands() {
},
'start-share-video': url => {
sendAnalytics(createApiEvent('share.video.start'));
const id = extractYoutubeIdOrURL(url, APP.store.getState()['features/shared-video'].allowedUrlDomains);
const id = extractYoutubeIdOrURL(url);

if (id) {
APP.store.dispatch(playSharedVideo(id));
Expand Down
10 changes: 10 additions & 0 deletions react/features/shared-video/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ export const SET_SHARED_VIDEO_STATUS = 'SET_SHARED_VIDEO_STATUS';
*/
export const RESET_SHARED_VIDEO_STATUS = 'RESET_SHARED_VIDEO_STATUS';

/**
* The type of the action which marks that the user had confirmed to play video.
*
* {
* type: SET_CONFIRM_SHOW_VIDEO
* }
*/
export const SET_CONFIRM_SHOW_VIDEO = 'SET_CONFIRM_SHOW_VIDEO';


/**
* The type of the action which signals to disable or enable the shared video
* button.
Expand Down
58 changes: 51 additions & 7 deletions react/features/shared-video/actions.any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,31 @@ import { getCurrentConference } from '../base/conference/functions';
import { openDialog } from '../base/dialog/actions';
import { getLocalParticipant } from '../base/participants/functions';

import { RESET_SHARED_VIDEO_STATUS, SET_ALLOWED_URL_DOMAINS, SET_SHARED_VIDEO_STATUS } from './actionTypes';
import { SharedVideoDialog } from './components';
import { isSharedVideoEnabled, isURLAllowedForSharedVideo } from './functions';
import {
RESET_SHARED_VIDEO_STATUS,
SET_ALLOWED_URL_DOMAINS,
SET_CONFIRM_SHOW_VIDEO,
SET_SHARED_VIDEO_STATUS
} from './actionTypes';
import { ShareVideoConfirmDialog, SharedVideoDialog } from './components';
import { PLAYBACK_START, PLAYBACK_STATUSES } from './constants';
import { isSharedVideoEnabled } from './functions';


/**
* Marks that user confirmed or not to play video.
*
* @param {boolean} value - The value to set.
* @returns {{
* type: SET_CONFIRM_SHOW_VIDEO,
* }}
*/
export function setConfirmShowVideo(value: boolean) {
return {
type: SET_CONFIRM_SHOW_VIDEO,
value
};
}

/**
* Resets the status of the shared video.
Expand Down Expand Up @@ -90,8 +112,7 @@ export function stopSharedVideo() {
*/
export function playSharedVideo(videoUrl: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (!isSharedVideoEnabled(getState())
|| !isURLAllowedForSharedVideo(videoUrl, getState()['features/shared-video'].allowedUrlDomains, true)) {
if (!isSharedVideoEnabled(getState())) {
return;
}
const conference = getCurrentConference(getState());
Expand All @@ -101,7 +122,7 @@ export function playSharedVideo(videoUrl: string) {

dispatch(setSharedVideoStatus({
videoUrl,
status: 'start',
status: PLAYBACK_START,
time: 0,
ownerId: localParticipant?.id
}));
Expand All @@ -120,7 +141,7 @@ export function toggleSharedVideo() {
const state = getState();
const { status = '' } = state['features/shared-video'];

if ([ 'playing', 'start', 'pause' ].includes(status)) {
if ([ PLAYBACK_STATUSES.PLAYING, PLAYBACK_START, PLAYBACK_STATUSES.PAUSED ].includes(status)) {
dispatch(stopSharedVideo());
} else {
dispatch(showSharedVideoDialog((id: string) => dispatch(playSharedVideo(id))));
Expand All @@ -143,3 +164,26 @@ export function setAllowedUrlDomians(allowedUrlDomains: Array<string>) {
allowedUrlDomains
};
}

/**
* Shows a confirmation dialog whether to play the external video link.
*
* @param {string} actor - The actor's name.
* @param {Function} onSubmit - The function to execute when confirmed.
*
* @returns {Function}
*/
export function showConfirmPlayingDialog(actor: String, onSubmit: Function) {
return (dispatch: IStore['dispatch']) => {
// shows only one dialog at a time
dispatch(setConfirmShowVideo(false));

dispatch(openDialog(ShareVideoConfirmDialog, {
actorName: actor,
onSubmit: () => {
dispatch(setConfirmShowVideo(true));
onSubmit();
}
}));
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ export default class AbstractSharedVideoDialog<S> extends Component < IProps, S
* @returns {boolean}
*/
_onSetVideoLink(link: string) {
const { _allowedUrlDomains, onPostSubmit } = this.props;
const { onPostSubmit } = this.props;

const id = extractYoutubeIdOrURL(link, _allowedUrlDomains);
const id = extractYoutubeIdOrURL(link);

if (!id) {
return false;
Expand Down
1 change: 1 addition & 0 deletions react/features/shared-video/components/index.native.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-ignore
export { default as SharedVideoDialog } from './native/SharedVideoDialog';
export { default as SharedVideoButton } from './native/SharedVideoButton';
export { default as ShareVideoConfirmDialog } from './native/ShareVideoConfirmDialog';
1 change: 1 addition & 0 deletions react/features/shared-video/components/index.web.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as SharedVideoDialog } from './web/SharedVideoDialog';
export { default as SharedVideoButton } from './web/SharedVideoButton';
export { default as ShareVideoConfirmDialog } from './web/ShareVideoConfirmDialog';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { useTranslation } from 'react-i18next';

import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
import { DialogProps } from '../../../base/dialog/constants';

interface IProps extends DialogProps {

/**
* The name of the remote participant that shared the video.
*/
actorName: string;

/**
* The function to execute when confirmed.
*/
onSubmit: () => void;
}

/**
* Dialog to confirm playing a video shared from a remote participant.
*
* @returns {JSX.Element}
*/
export default function ShareVideoConfirmDialog({ actorName, onSubmit }: IProps): JSX.Element {
const { t } = useTranslation();

return (
<ConfirmDialog
cancelLabel = 'dialog.Cancel'
confirmLabel = 'dialog.Ok'
descriptionKey = 'dialog.shareVideoConfirmPlay'
onSubmit = { onSubmit }
title = { t('dialog.shareVideoConfirmPlayTitle', {
name: actorName
}) } />
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class YoutubeVideoManager extends AbstractVideoManager<IState> {
});
}

if (event === 'playing') {
if (event === PLAYBACK_STATUSES.PLAYING) {
this.setState({
paused: false
}, () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { useTranslation } from 'react-i18next';

import { DialogProps } from '../../../base/dialog/constants';
import Dialog from '../../../base/ui/components/web/Dialog';

interface IProps extends DialogProps {

/**
* The name of the remote participant that shared the video.
*/
actorName: string;

/**
* The function to execute when confirmed.
*/
onSubmit: () => void;
}

/**
* Dialog to confirm playing a video shared from a remote participant.
*
* @returns {JSX.Element}
*/
export default function ShareVideoConfirmDialog({ actorName, onSubmit }: IProps): JSX.Element {
const { t } = useTranslation();

return (
<Dialog
onSubmit = { onSubmit }
title = { t('dialog.shareVideoConfirmPlayTitle', {
name: actorName
}) }>
<div>
{ t('dialog.shareVideoConfirmPlay') }
</div>
</Dialog>
);
}
5 changes: 5 additions & 0 deletions react/features/shared-video/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export const PLAYBACK_STATUSES = {
STOPPED: 'stop'
};

/**
* Playback start state.
*/
export const PLAYBACK_START = 'start';

/**
* The domain for youtube URLs.
*/
Expand Down
24 changes: 13 additions & 11 deletions react/features/shared-video/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { toState } from '../base/redux/functions';

import {
ALLOW_ALL_URL_DOMAINS,
PLAYBACK_START,
PLAYBACK_STATUSES,
VIDEO_PLAYER_PARTICIPANT_NAME,
YOUTUBE_PLAYER_PARTICIPANT_NAME,
YOUTUBE_URL_DOMAIN
Expand Down Expand Up @@ -35,7 +37,7 @@ function getYoutubeId(url: string) {
* @returns {boolean}
*/
export function isSharingStatus(status: string) {
return [ 'playing', 'pause', 'start' ].includes(status);
return [ PLAYBACK_STATUSES.PLAYING, PLAYBACK_STATUSES.PAUSED, PLAYBACK_START ].includes(status);
}


Expand Down Expand Up @@ -63,10 +65,9 @@ export function isVideoPlaying(stateful: IStateful): boolean {
* Extracts a Youtube id or URL from the user input.
*
* @param {string} input - The user input.
* @param {Array<string>} allowedUrlDomains - The allowed URL domains for shared video.
* @returns {string|undefined}
*/
export function extractYoutubeIdOrURL(input: string, allowedUrlDomains?: Array<string>) {
export function extractYoutubeIdOrURL(input: string) {
if (!input) {
return;
}
Expand All @@ -77,15 +78,17 @@ export function extractYoutubeIdOrURL(input: string, allowedUrlDomains?: Array<s
return;
}

if (areYoutubeURLsAllowedForSharedVideo(allowedUrlDomains)) {
const youtubeId = getYoutubeId(trimmedLink);
const youtubeId = getYoutubeId(trimmedLink);

if (youtubeId) {
return youtubeId;
}
if (youtubeId) {
return youtubeId;
}

if (!isURLAllowedForSharedVideo(trimmedLink, allowedUrlDomains)) {
// Check if the URL is valid, native may crash otherwise.
try {
// eslint-disable-next-line no-new
new URL(trimmedLink);
} catch (_) {
return;
}

Expand All @@ -101,10 +104,9 @@ export function extractYoutubeIdOrURL(input: string, allowedUrlDomains?: Array<s
export function isSharedVideoEnabled(stateful: IStateful) {
const state = toState(stateful);

const { allowedUrlDomains = [] } = toState(stateful)['features/shared-video'];
const { disableThirdPartyRequests = false } = state['features/base/config'];

return !disableThirdPartyRequests && allowedUrlDomains.length > 0;
return !disableThirdPartyRequests;
}

/**
Expand Down
47 changes: 36 additions & 11 deletions react/features/shared-video/middleware.any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SET_CONFIG } from '../base/config/actionTypes';
import { MEDIA_TYPE } from '../base/media/constants';
import { PARTICIPANT_LEFT } from '../base/participants/actionTypes';
import { participantJoined, participantLeft, pinParticipant } from '../base/participants/actions';
import { getLocalParticipant, getParticipantById } from '../base/participants/functions';
import { getLocalParticipant, getParticipantById, getParticipantDisplayName } from '../base/participants/functions';
import { FakeParticipant } from '../base/participants/types';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { SET_DYNAMIC_BRANDING_DATA } from '../dynamic-branding/actionTypes';
Expand All @@ -17,10 +17,12 @@ import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionType
import {
resetSharedVideoStatus,
setAllowedUrlDomians,
setSharedVideoStatus
setSharedVideoStatus,
showConfirmPlayingDialog
} from './actions.any';
import {
DEFAULT_ALLOWED_URL_DOMAINS,
PLAYBACK_START,
PLAYBACK_STATUSES,
SHARED_VIDEO,
VIDEO_PLAYER_PARTICIPANT_NAME
Expand Down Expand Up @@ -53,18 +55,33 @@ MiddlewareRegistry.register(store => next => action => {
from: string; muted: string; state: string; time: string; }; value: string; }) => {
const state = getState();

if (!isURLAllowedForSharedVideo(value, getState()['features/shared-video'].allowedUrlDomains, true)) {
logger.debug(`Shared Video: Received a not allowed URL ${value}`);

return;
}

const { from } = attributes;
const sharedVideoStatus = attributes.state;

if (isSharingStatus(sharedVideoStatus)) {
handleSharingVideoStatus(store, value, attributes, conference);
} else if (sharedVideoStatus === 'stop') {
// confirmShowVideo is undefined the first time we receive
// when confirmShowVideo is false we ignore everything except stop that resets it
if (getState()['features/shared-video'].confirmShowVideo === false) {
return;
}

if (isURLAllowedForSharedVideo(value)
|| localParticipantId === from
|| getState()['features/shared-video'].confirmShowVideo) { // if confirmed skip asking again
handleSharingVideoStatus(store, value, attributes, conference);
} else {
dispatch(showConfirmPlayingDialog(getParticipantDisplayName(getState(), from), () => {

handleSharingVideoStatus(store, value, attributes, conference);

return true; // on mobile this is used to close the dialog
}));
}

return;
}

if (sharedVideoStatus === 'stop') {
const videoParticipant = getParticipantById(state, value);

dispatch(participantLeft(value, conference, {
Expand Down Expand Up @@ -189,8 +206,16 @@ function handleSharingVideoStatus(store: IStore, videoUrl: string,
const { dispatch, getState } = store;
const localParticipantId = getLocalParticipant(getState())?.id;
const oldStatus = getState()['features/shared-video']?.status ?? '';
const oldVideoUrl = getState()['features/shared-video'].videoUrl;

if (oldVideoUrl && oldVideoUrl !== videoUrl) {
logger.warn(
`User with id: ${localParticipantId} sent videoUrl: ${videoUrl} while we are playing: ${oldVideoUrl}`);

return;
}

if (state === 'start' || ![ 'playing', 'pause', 'start' ].includes(oldStatus)) {
if (state === PLAYBACK_START && !isSharingStatus(oldStatus)) {
const youtubeId = videoUrl.match(/http/) ? false : videoUrl;
const avatarURL = youtubeId ? `https://img.youtube.com/vi/${youtubeId}/0.jpg` : '';

Expand Down
Loading

0 comments on commit 3f7c3b8

Please sign in to comment.