Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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